diff --git a/inventory-orders-service/.sdkmanrc b/inventory-orders-service/.sdkmanrc index 74c0edb..6d54fb2 100644 --- a/inventory-orders-service/.sdkmanrc +++ b/inventory-orders-service/.sdkmanrc @@ -1 +1 @@ -java=17.0.2-open \ No newline at end of file +java=17.0.8-graal diff --git a/inventory-orders-service/CLAUDE.md b/inventory-orders-service/CLAUDE.md new file mode 100644 index 0000000..385f415 --- /dev/null +++ b/inventory-orders-service/CLAUDE.md @@ -0,0 +1,87 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +This is an example project demonstrating Flamingock, a Change-as-Code execution engine. It simulates an e-commerce inventory & orders service that coordinates versioned changes across multiple target systems: MongoDB, Kafka Schema Registry, and LaunchDarkly (used here as an external configuration system, not runtime flag evaluation). + +**What Flamingock is:** A Change-as-Code execution engine that safely, deterministically, and auditably applies versioned changes to external systems at application startup. The goal is to ensure that all dependent external systems evolve in a synchronized and auditable way with the application. Think "Liquibase for all external systems, with auditability and safety as first-class constraints." + +**What Flamingock is NOT:** Not a database migration tool, not Infrastructure-as-Code (Terraform/Pulumi), not a CI/CD pipeline, not runtime feature flagging. + +## Build Commands + +```bash +./gradlew build # Build the project +./gradlew run # Run the application (executes pending changes at startup) +./gradlew test # Run integration tests with Testcontainers (no external services needed) +./gradlew clean # Clean build artifacts +``` + +## Running the Application + +**Option 1: With Docker Compose (for manual execution)** +```bash +docker-compose up -d # Start MongoDB, Kafka, Schema Registry, LaunchDarkly mock +./gradlew run # Execute pending changes +``` + +**Option 2: With Testcontainers (for testing)** +```bash +./gradlew test # Starts all containers automatically via Testcontainers +``` + +## Architecture + +### Flamingock Core Concepts + +- **Change** - The atomic unit of change. Defines what system it targets, what version it represents, and how it is applied. Changes are versioned, ordered, immutable once applied, and should be idempotent. +- **Audit Store** - Records execution state (STARTED, COMPLETED). Intentionally separate from target systems to enable safe retries, manual intervention, and cross-system governance. +- **Target System** - The external system being modified. Flamingock does not assume target systems are transactional; safety is enforced via execution tracking. + +### This Project's Setup + +- Entry point: `InventoryOrdersApp.java` with `@EnableFlamingock` annotation +- Changes discovered from `io.flamingock.examples.inventory.changes` package +- Uses compile-time annotation processing via `flamingock-processor` + +### Target Systems (defined in `TargetSystems.java`) +1. **mongodb-inventory** - MongoDB operations (also used as Audit Store in this example; in real setups, the Audit Store may be a dedicated system) +2. **kafka-inventory** - Kafka Schema Registry operations +3. **toggle-inventory** - LaunchDarkly as external configuration system (not runtime flag evaluation) + +### Change Naming Convention +In this example repository, changes follow the pattern: `_NNNN__[target]_[description].java` +- Example: `_0001__mongodb_addDiscountCodeFieldToOrders.java` +- Auto-ordered by numeric prefix +- Each change class has an `@Apply` method and may optionally define a `@Rollback` for reconciliation or manual recovery + +### Utility Classes (`util/` package) +- `MongoDBUtil` - MongoDB connection string builder +- `KafkaSchemaManager` - Schema Registry REST operations +- `LaunchDarklyClient` - Feature flag CRUD via REST API +- `ConfigFileManager` - YAML config file handling + +## Technology Stack +- Java 17 +- Flamingock v1.0.0-beta.5 +- MongoDB Driver Sync v5.5.1 +- Apache Kafka v3.7.0 with Avro v1.11.3 +- Confluent Schema Registry Client v7.5.0 +- OkHttp3 v4.12.0 (for REST calls) +- Testcontainers v2.0.2 (for integration tests) + +## Infrastructure Services (docker-compose.yml) +| Service | Port | Purpose | +|-------------------|-------|-----------------------------------------------| +| MongoDB | 27017 | Target system & Audit Store (in this example) | +| Kafka | 9092 | Event streaming | +| Schema Registry | 8081 | Avro schema management | +| LaunchDarkly Mock | 8765 | External configuration system simulation | + +## Testing +Integration tests use Testcontainers to spin up isolated Docker containers: +- `SuccessExecutionTest.java` tests all 7 changes +- Tests verify MongoDB documents, Kafka schemas, and audit log entries +- No external Docker Compose required for tests diff --git a/inventory-orders-service/META-INF/MANIFEST.MF b/inventory-orders-service/META-INF/MANIFEST.MF new file mode 100644 index 0000000..a205b10 --- /dev/null +++ b/inventory-orders-service/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Main-Class: io.flamingock.examples.inventory.InventoryOrdersApp + diff --git a/inventory-orders-service/README.md b/inventory-orders-service/README.md index ba0b09b..a38cc0e 100644 --- a/inventory-orders-service/README.md +++ b/inventory-orders-service/README.md @@ -9,15 +9,15 @@ ___ This example simulates an **e-commerce service** that manages inventory and orders. It demonstrates how Flamingock coordinates multiple target systems in lockstep using the **Change-as-Code** approach. -The story begins when the **marketing team** launches a promotional campaign that requires support for **discount codes**. +The story begins when the **marketing team** launches a promotional campaign that requires support for **discount codes**. To implement this feature safely, the product and engineering teams plan a sequence of deployments, each introducing incremental changes in a controlled, auditable way. -As development progresses, the **sales team** also requests the ability to quickly search and report on orders by discount code. +As development progresses, the **sales team** also requests the ability to quickly search and report on orders by discount code. This leads the engineers to add an index on the new field as part of the rollout, ensuring the system remains both functional and performant. ### Lifecycle of the feature rollout -1. **Initial deployment** +1. **Initial deployment** *Business driver:* prepare the system for discounts while keeping the feature hidden. - Add the `discountCode` field to the `orders` collection in **MongoDB**. - Update the `OrderCreated` schema in **Kafka** to include the same field. @@ -28,7 +28,7 @@ This leads the engineers to add an index on the new field as part of the rollout - [`UpdateOrderCreatedSchema`](#updateordercreatedschema) - [`AddFeatureFlagDiscounts`](#addfeatureflagdiscounts) -2. **Second deployment** +2. **Second deployment** *Business driver:* ensure existing and new orders remain consistent. - Backfill existing orders with a default `discountCode` (e.g. `"NONE"`). - Application logic begins to populate the field for new orders, still hidden behind the flag. @@ -37,7 +37,7 @@ This leads the engineers to add an index on the new field as part of the rollout - [`BackfillDiscountsForExistingOrders`](#backfilldiscountsforexistingorders) - [`AddIndexOnDiscountCode`](#addindexondiscountcode) -3. **Runtime activation (no deployment)** +3. **Runtime activation (no deployment)** *Business driver:* marketing activates discounts for customers. - The feature flag is enabled at runtime using a feature-flag tool (e.g. Unleash, LaunchDarkly). - No redeployment is required — the system is already prepared. @@ -67,6 +67,9 @@ This example showcases Flamingock’s ability to: - [Prerequisites](#prerequisites) - [Dependencies](#dependencies) - [How to Run this Example](#how-to-run-this-example) + - [Option 1: Run the Application (Recommended)](#option-1-run-the-application-recommended) + - [Option 2: Run Tests](#option-2-run-tests) + - [Option 3: Run with GraalVM Native Image (Optional)](#option-3-run-with-graalvm-native-image-optional) - [Proven Functionalities](#proven-functionalities) - [Implemented Changes](#implemented-changes) - [Contributing](#contributing) @@ -196,7 +199,7 @@ curl http://localhost:8081/subjects/order-created-value/versions Check the audit logs in MongoDB: ```bash -docker exec -it inventory-mongodb mongosh inventory --eval 'db.flamingockAuditLogs.find().pretty()' +docker exec -it inventory-mongodb mongosh inventory --eval 'db.flamingockAuditLog.find().pretty()' ``` 5. **Clean up when done:** @@ -211,6 +214,97 @@ Run the integration tests with Testcontainers (no Docker Compose needed): ./gradlew test ``` +### Option 3: Run with GraalVM Native Image (Optional) + +If you want to showcase Flamingock running as a GraalVM native image, you can follow these **optional** steps. The regular JVM flow above still works as-is. + +For full details, see the official docs: https://docs.flamingock.io/frameworks/graalvm + +#### 1. Use a GraalVM Java distribution + +Using SDKMAN: + +```bash +sdk env install # uses .sdkmanrc in this folder +sdk use java 17.0.8-graal # or any installed GraalVM distribution compatible with your setup +``` + +The default `.sdkmanrc` included with this example already uses Java 17 with GraalVM. + +#### 2. Ensure GraalVM support dependencies are present + +This example already includes the Flamingock GraalVM integration and resource configuration: + +- `build.gradle.kts` contains (commented): + - `implementation("io.flamingock:flamingock-graalvm:$flamingockVersion")` +- `resource-config.json` in the project root includes: + - `META-INF/flamingock/metadata.json` resources required at native-image time + +If you copy this example to your own project, make sure you add the same pieces (or follow the docs linked above). + +#### 3. Build the fat (uber) JAR + +First build the application as usual, which also creates a **fat / uber JAR** bundling all runtime dependencies and a `Main-Class` manifest entry: + +```bash +./gradlew clean build +``` + +The `jar` task in `build.gradle.kts` is configured like this: + +```kotlin +tasks.named("jar") { + manifest { + attributes["Main-Class"] = "io.flamingock.examples.inventory.InventoryOrdersApp" + } + + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + + from(sourceSets.main.get().output) + + from({ + configurations.runtimeClasspath.get().map { if (it.isDirectory) it else zipTree(it) } + }) +} +``` + +This produces an executable uber JAR under `build/libs/` (for example `build/libs/inventory-orders-service-1.0-SNAPSHOT.jar`). + +> **Why this matters for GraalVM** +> +> GraalVM's `native-image -jar` mode expects a JAR that: +> - has a valid `Main-Class` manifest attribute, and +> - contains all the classes and dependencies reachable from that entry point. +> +> The fat/uber JAR configuration above ensures those requirements are met, which is essential for the native-image step to work reliably. + +#### 4. Create the native image + +From the project root, run (adjust the JAR name and output binary name if needed): + +```bash +native-image \ + --no-fallback \ + --features=io.flamingock.graalvm.RegistrationFeature \ + -H:ResourceConfigurationFiles=resource-config.json \ + -H:+ReportExceptionStackTraces \ + --initialize-at-build-time=org.slf4j.simple \ + -jar build/libs/inventory-orders-service-1.0-SNAPSHOT.jar \ + inventory-orders-service +``` + +This uses Flamingock's GraalVM feature to automatically register all required reflection metadata. + +#### 5. Run the native image + +With Docker Compose infrastructure already running (see [Option 1](#option-1-run-the-application-recommended)), start the native binary: + +```bash +./inventory-orders-service +``` + +The application will execute the same Flamingock migrations as when running on the regular JVM, but with GraalVM-native startup and footprint characteristics. + ## Troubleshooting ### Schema Registry Connection Issues @@ -270,15 +364,15 @@ This example demonstrates the following Flamingock capabilities: ## Implemented Changes -| Deployment Step | Change Name | Target Systems | Operation | Description | -|--------------------------------|-------------------------------------------------------------------------------------|-----------------------|-----------------------------------|----------------------------------------------------------------------------------------------| -| [Initial](#initial-deployment) | `AddDiscountCodeFieldToOrders` | MongoDB | Alter collection / add field | Adds `discountCode` (nullable) to the orders collection | -| [Initial](#initial-deployment) | `UpdateOrderCreatedSchema` | Kafka Schema Registry | Register new schema version | Publishes a new version of the OrderCreated event schema including discountCode | +| Deployment Step | Change Name | Target Systems | Operation | Description | +|--------------------------------|-------------------------------------------------------------------------------------|-----------------------|-----------------------------------|----------------------------------------------------------------------------------------------| +| [Initial](#initial-deployment) | `AddDiscountCodeFieldToOrders` | MongoDB | Alter collection / add field | Adds `discountCode` (nullable) to the orders collection | +| [Initial](#initial-deployment) | `UpdateOrderCreatedSchema` | Kafka Schema Registry | Register new schema version | Publishes a new version of the OrderCreated event schema including discountCode | | [Initial](#initial-deployment) | `AddFeatureFlagDiscounts` | LaunchDarkly API | Create flags | Creates feature flags for discount functionality using LaunchDarkly Management API | | [Second](#second-deployment) | `BackfillDiscountsForExistingOrders` | MongoDB | Update | Updates existing orders with discountCode = "NONE" | | [Second](#second-deployment) | `AddIndexOnDiscountCode` | MongoDB | Create index | Creates an index on discountCode to support reporting and efficient lookups | | [Final](#final-deployment) | `CleanupFeatureFlagDiscounts` | LaunchDarkly API | Archive flags | Archives temporary feature flags once the feature is permanent and code guards are removed | -| [Final](#final-deployment) | `CleanupOldSchemaVersion` | Kafka Schema Registry | Disable/delete old schema version | Removes outdated schema once all consumers have migrated to the new version | +| [Final](#final-deployment) | `CleanupOldSchemaVersion` | Kafka Schema Registry | Disable/delete old schema version | Removes outdated schema once all consumers have migrated to the new version | ## Example Output @@ -286,7 +380,7 @@ After running the migrations, you'll see: - Orders in MongoDB with discount fields populated - Two schema versions in Schema Registry (V1 and V2) - LaunchDarkly Management API calls for feature flag creation/archival via mock server -- Complete audit trail in the flamingockAuditLogs collection +- Complete audit trail in the flamingockAuditLog collection ## Architecture Notes @@ -323,4 +417,4 @@ This repository is licensed under the [Apache License 2.0](../LICENSE.md). ## Explore, experiment, and empower your projects with Flamingock! -Let us know what you think or where you'd like to see Flamingock used next. \ No newline at end of file +Let us know what you think or where you'd like to see Flamingock used next. diff --git a/inventory-orders-service/build.gradle.kts b/inventory-orders-service/build.gradle.kts index 2861905..7290db4 100644 --- a/inventory-orders-service/build.gradle.kts +++ b/inventory-orders-service/build.gradle.kts @@ -5,6 +5,8 @@ plugins { java application idea + id("org.springframework.boot") version "3.2.0" + id("io.spring.dependency-management") version "1.1.4" } java { @@ -26,7 +28,7 @@ repositories { group = "io.flamingock" version = "1.0-SNAPSHOT" -val flamingockVersion = "1.0.0-beta.1" +val flamingockVersion = "1.0.0-beta.5" logger.lifecycle("Building with flamingock version: $flamingockVersion") val mongodbVersion = "5.5.1" @@ -39,6 +41,13 @@ dependencies { // Flamingock Dependencies implementation(platform("io.flamingock:flamingock-community-bom:$flamingockVersion")) implementation("io.flamingock:flamingock-community") + implementation("io.flamingock:flamingock-springboot-integration") + + + // Optional: enable GraalVM native image support for Flamingock + // See: https://docs.flamingock.io/frameworks/graalvm + // Uncomment + // implementation("io.flamingock:flamingock-graalvm:$flamingockVersion") annotationProcessor("io.flamingock:flamingock-processor:$flamingockVersion") // MongoDB dependencies @@ -58,21 +67,38 @@ dependencies { // HTTP client for LaunchDarkly Management API implementation("com.squareup.okhttp3:okhttp:4.12.0") +// Spring Boot + implementation("org.springframework.boot:spring-boot-starter-web") + // Others dependencies needed for this example - implementation("org.slf4j:slf4j-simple:2.0.6") +// implementation("org.slf4j:slf4j-simple:2.0.6") // Commented out - Spring Boot provides logging testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.2") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.9.2") - testImplementation("org.testcontainers:mongodb:1.21.3") - testImplementation("org.testcontainers:kafka:1.21.3") - testImplementation("org.testcontainers:junit-jupiter:1.21.3") + testImplementation("org.testcontainers:testcontainers-mongodb:2.0.2") + testImplementation("org.testcontainers:testcontainers-kafka:2.0.2") + testImplementation("org.testcontainers:testcontainers-junit-jupiter:2.0.2") } application { mainClass = "io.flamingock.examples.inventory.InventoryOrdersApp" } +tasks.named("jar") { + manifest { + attributes["Main-Class"] = "io.flamingock.examples.inventory.InventoryOrdersApp" + } + + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + + from(sourceSets.main.get().output) + + from({ + configurations.runtimeClasspath.get().map { if (it.isDirectory) it else zipTree(it) } + }) +} + tasks.withType { options.compilerArgs.add("-parameters") } diff --git a/inventory-orders-service/resource-config.json b/inventory-orders-service/resource-config.json new file mode 100644 index 0000000..599fa54 --- /dev/null +++ b/inventory-orders-service/resource-config.json @@ -0,0 +1,7 @@ +{ + "resources": { + "includes": [ + { "pattern": "META-INF/flamingock/metadata.json" } + ] + } +} diff --git a/inventory-orders-service/src/main/java/io/flamingock/examples/inventory/FlamingockConfig.java b/inventory-orders-service/src/main/java/io/flamingock/examples/inventory/FlamingockConfig.java new file mode 100644 index 0000000..9d1eac2 --- /dev/null +++ b/inventory-orders-service/src/main/java/io/flamingock/examples/inventory/FlamingockConfig.java @@ -0,0 +1,41 @@ +package io.flamingock.examples.inventory; + +import com.mongodb.client.MongoClient; +import io.flamingock.community.mongodb.sync.driver.MongoDBSyncAuditStore; +import io.flamingock.examples.inventory.util.MongoDBUtil; +import io.flamingock.internal.core.store.CommunityAuditStore; +import io.flamingock.targetsystem.nontransactional.NonTransactionalTargetSystem; +import io.flamingock.targetystem.mongodb.sync.MongoDBSyncTargetSystem; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class FlamingockConfig { + + @Bean + public MongoClient mongoClient() { + return MongoDBUtil.getMongoClient("mongodb://localhost:27017/"); + } + + @Bean + public MongoDBSyncTargetSystem mongoDBSyncTargetSystem(MongoClient mongoClient) { + return new MongoDBSyncTargetSystem(TargetSystems.MONGODB_TARGET_SYSTEM, mongoClient, TargetSystems.DATABASE_NAME); + } + + @Bean + public NonTransactionalTargetSystem kafkaTargetSystem() throws Exception { + return TargetSystems.kafkaTargetSystem(); + } + + @Bean + public NonTransactionalTargetSystem toggleTargetSystem() { + return TargetSystems.toggleTargetSystem(); + } + + + //This could return any of the available community audit stores + @Bean + public CommunityAuditStore auditStore(MongoDBSyncTargetSystem mongoDBSyncTargetSystem) { + return MongoDBSyncAuditStore.from(mongoDBSyncTargetSystem); + } +} diff --git a/inventory-orders-service/src/main/java/io/flamingock/examples/inventory/InventoryOrdersApp.java b/inventory-orders-service/src/main/java/io/flamingock/examples/inventory/InventoryOrdersApp.java index 9e70768..8d74581 100644 --- a/inventory-orders-service/src/main/java/io/flamingock/examples/inventory/InventoryOrdersApp.java +++ b/inventory-orders-service/src/main/java/io/flamingock/examples/inventory/InventoryOrdersApp.java @@ -16,17 +16,16 @@ package io.flamingock.examples.inventory; -import com.mongodb.client.MongoClient; import io.flamingock.api.annotations.EnableFlamingock; import io.flamingock.api.annotations.Stage; import io.flamingock.community.Flamingock; import io.flamingock.community.mongodb.sync.driver.MongoDBSyncAuditStore; -import io.flamingock.examples.inventory.util.MongoDBUtil; -import io.flamingock.examples.inventory.util.TargetSystems; import io.flamingock.internal.core.store.CommunityAuditStore; +import io.flamingock.targetystem.mongodb.sync.MongoDBSyncTargetSystem; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; -import static io.flamingock.examples.inventory.util.TargetSystems.DATABASE_NAME; - +@SpringBootApplication @EnableFlamingock( stages = { @Stage(name = "inventory", location = "io.flamingock.examples.inventory.changes") @@ -36,22 +35,8 @@ public class InventoryOrdersApp { public static void main(String[] args) throws Exception { - Flamingock.builder() - .setAuditStore(auditStore()) - .addTargetSystems( - TargetSystems.mongoDBSyncTargetSystem(), - TargetSystems.kafkaTargetSystem(), - TargetSystems.toggleTargetSystem()) - .build() - .run(); - - } - - //This could return any of the available community audit stores - private static CommunityAuditStore auditStore() { - MongoClient mongoClient = MongoDBUtil.getMongoClient("mongodb://localhost:27017/"); - return new MongoDBSyncAuditStore(mongoClient, DATABASE_NAME); + SpringApplication.run(InventoryOrdersApp.class, args); } -} \ No newline at end of file +} diff --git a/inventory-orders-service/src/main/java/io/flamingock/examples/inventory/util/TargetSystems.java b/inventory-orders-service/src/main/java/io/flamingock/examples/inventory/TargetSystems.java similarity index 92% rename from inventory-orders-service/src/main/java/io/flamingock/examples/inventory/util/TargetSystems.java rename to inventory-orders-service/src/main/java/io/flamingock/examples/inventory/TargetSystems.java index 0ef22b0..5321522 100644 --- a/inventory-orders-service/src/main/java/io/flamingock/examples/inventory/util/TargetSystems.java +++ b/inventory-orders-service/src/main/java/io/flamingock/examples/inventory/TargetSystems.java @@ -1,8 +1,11 @@ -package io.flamingock.examples.inventory.util; +package io.flamingock.examples.inventory; import com.mongodb.client.MongoClient; import io.confluent.kafka.schemaregistry.client.CachedSchemaRegistryClient; import io.confluent.kafka.schemaregistry.client.SchemaRegistryClient; +import io.flamingock.examples.inventory.util.KafkaSchemaManager; +import io.flamingock.examples.inventory.util.LaunchDarklyClient; +import io.flamingock.examples.inventory.util.MongoDBUtil; import io.flamingock.targetsystem.nontransactional.NonTransactionalTargetSystem; import io.flamingock.targetystem.mongodb.sync.MongoDBSyncTargetSystem; import org.apache.kafka.clients.admin.AdminClient; diff --git a/inventory-orders-service/src/main/java/io/flamingock/examples/inventory/changes/_0001__mongodb_addDiscountCodeFieldToOrders.java b/inventory-orders-service/src/main/java/io/flamingock/examples/inventory/changes/_0001__mongodb_addDiscountCodeFieldToOrders.java index 356fab8..7f9e622 100644 --- a/inventory-orders-service/src/main/java/io/flamingock/examples/inventory/changes/_0001__mongodb_addDiscountCodeFieldToOrders.java +++ b/inventory-orders-service/src/main/java/io/flamingock/examples/inventory/changes/_0001__mongodb_addDiscountCodeFieldToOrders.java @@ -27,7 +27,7 @@ import java.util.Arrays; import java.util.Collections; -import static io.flamingock.examples.inventory.util.TargetSystems.MONGODB_TARGET_SYSTEM; +import static io.flamingock.examples.inventory.TargetSystems.MONGODB_TARGET_SYSTEM; @TargetSystem(id = MONGODB_TARGET_SYSTEM) @Change(id = "add-discount-code-field-to-orders", author = "flamingock-team") diff --git a/inventory-orders-service/src/main/java/io/flamingock/examples/inventory/changes/_0002__kafka_updateOrderCreatedSchema.java b/inventory-orders-service/src/main/java/io/flamingock/examples/inventory/changes/_0002__kafka_updateOrderCreatedSchema.java index e6c0af2..74b63b6 100644 --- a/inventory-orders-service/src/main/java/io/flamingock/examples/inventory/changes/_0002__kafka_updateOrderCreatedSchema.java +++ b/inventory-orders-service/src/main/java/io/flamingock/examples/inventory/changes/_0002__kafka_updateOrderCreatedSchema.java @@ -22,7 +22,7 @@ import io.flamingock.api.annotations.TargetSystem; import io.flamingock.examples.inventory.util.KafkaSchemaManager; -import static io.flamingock.examples.inventory.util.TargetSystems.KAFKA_TARGET_SYSTEM; +import static io.flamingock.examples.inventory.TargetSystems.KAFKA_TARGET_SYSTEM; @TargetSystem(id =KAFKA_TARGET_SYSTEM) @Change(id = "update-order-created-schema", author = "flamingock-team", transactional = false) diff --git a/inventory-orders-service/src/main/java/io/flamingock/examples/inventory/changes/_0003__toggle_addFeatureFlagDiscounts.java b/inventory-orders-service/src/main/java/io/flamingock/examples/inventory/changes/_0003__toggle_addFeatureFlagDiscounts.java index 8d452c7..b3eb846 100644 --- a/inventory-orders-service/src/main/java/io/flamingock/examples/inventory/changes/_0003__toggle_addFeatureFlagDiscounts.java +++ b/inventory-orders-service/src/main/java/io/flamingock/examples/inventory/changes/_0003__toggle_addFeatureFlagDiscounts.java @@ -22,7 +22,7 @@ import io.flamingock.api.annotations.TargetSystem; import io.flamingock.examples.inventory.util.LaunchDarklyClient; -import static io.flamingock.examples.inventory.util.TargetSystems.FEATURE_FLAG_TARGET_SYSTEM; +import static io.flamingock.examples.inventory.TargetSystems.FEATURE_FLAG_TARGET_SYSTEM; @TargetSystem(id = FEATURE_FLAG_TARGET_SYSTEM) @Change(id = "add-feature-flag-discounts", author = "flamingock-team", transactional = false) diff --git a/inventory-orders-service/src/main/java/io/flamingock/examples/inventory/changes/_0004__mongodb_backfillDiscountsForExistingOrders.java b/inventory-orders-service/src/main/java/io/flamingock/examples/inventory/changes/_0004__mongodb_backfillDiscountsForExistingOrders.java index 235239d..8a73d1f 100644 --- a/inventory-orders-service/src/main/java/io/flamingock/examples/inventory/changes/_0004__mongodb_backfillDiscountsForExistingOrders.java +++ b/inventory-orders-service/src/main/java/io/flamingock/examples/inventory/changes/_0004__mongodb_backfillDiscountsForExistingOrders.java @@ -28,7 +28,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import static io.flamingock.examples.inventory.util.TargetSystems.MONGODB_TARGET_SYSTEM; +import static io.flamingock.examples.inventory.TargetSystems.MONGODB_TARGET_SYSTEM; @TargetSystem(id = MONGODB_TARGET_SYSTEM) @Change(id = "backfill-discounts-for-existing-orders", author = "flamingock-team", transactional = true) diff --git a/inventory-orders-service/src/main/java/io/flamingock/examples/inventory/changes/_0005__mongodb_addIndexOnDiscountCode.java b/inventory-orders-service/src/main/java/io/flamingock/examples/inventory/changes/_0005__mongodb_addIndexOnDiscountCode.java index 4f96a8c..c167485 100644 --- a/inventory-orders-service/src/main/java/io/flamingock/examples/inventory/changes/_0005__mongodb_addIndexOnDiscountCode.java +++ b/inventory-orders-service/src/main/java/io/flamingock/examples/inventory/changes/_0005__mongodb_addIndexOnDiscountCode.java @@ -28,7 +28,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import static io.flamingock.examples.inventory.util.TargetSystems.MONGODB_TARGET_SYSTEM; +import static io.flamingock.examples.inventory.TargetSystems.MONGODB_TARGET_SYSTEM; @TargetSystem(id = MONGODB_TARGET_SYSTEM) @Change(id = "add-index-on-discount-code", author = "flamingock-team", transactional = true) diff --git a/inventory-orders-service/src/main/java/io/flamingock/examples/inventory/changes/_0006__toggle_cleanupFeatureFlagDiscounts.java b/inventory-orders-service/src/main/java/io/flamingock/examples/inventory/changes/_0006__toggle_cleanupFeatureFlagDiscounts.java index da90bc1..db07ee9 100644 --- a/inventory-orders-service/src/main/java/io/flamingock/examples/inventory/changes/_0006__toggle_cleanupFeatureFlagDiscounts.java +++ b/inventory-orders-service/src/main/java/io/flamingock/examples/inventory/changes/_0006__toggle_cleanupFeatureFlagDiscounts.java @@ -24,7 +24,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import static io.flamingock.examples.inventory.util.TargetSystems.FEATURE_FLAG_TARGET_SYSTEM; +import static io.flamingock.examples.inventory.TargetSystems.FEATURE_FLAG_TARGET_SYSTEM; @TargetSystem(id = FEATURE_FLAG_TARGET_SYSTEM) @Change(id = "cleanup-feature-flag-discounts", author = "flamingock-team", transactional = false) diff --git a/inventory-orders-service/src/main/java/io/flamingock/examples/inventory/changes/_0007__kafka_leanupOldSchemaVersion.java b/inventory-orders-service/src/main/java/io/flamingock/examples/inventory/changes/_0007__kafka_leanupOldSchemaVersion.java index ba04d92..8e8505b 100644 --- a/inventory-orders-service/src/main/java/io/flamingock/examples/inventory/changes/_0007__kafka_leanupOldSchemaVersion.java +++ b/inventory-orders-service/src/main/java/io/flamingock/examples/inventory/changes/_0007__kafka_leanupOldSchemaVersion.java @@ -24,7 +24,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import static io.flamingock.examples.inventory.util.TargetSystems.KAFKA_TARGET_SYSTEM; +import static io.flamingock.examples.inventory.TargetSystems.KAFKA_TARGET_SYSTEM; @TargetSystem(id =KAFKA_TARGET_SYSTEM) @Change(id = "cleanup-old-schema-version", author = "flamingock-team", transactional = false) diff --git a/inventory-orders-service/src/main/java/io/flamingock/examples/inventory/order/Order.java b/inventory-orders-service/src/main/java/io/flamingock/examples/inventory/order/Order.java new file mode 100644 index 0000000..ce99145 --- /dev/null +++ b/inventory-orders-service/src/main/java/io/flamingock/examples/inventory/order/Order.java @@ -0,0 +1,14 @@ +package io.flamingock.examples.inventory.order; + +import java.util.List; + +public record Order( + String orderId, + String customerId, + List items, + double total, + String status, + String createdAt, + String discountCode, + boolean discountApplied +) {} diff --git a/inventory-orders-service/src/main/java/io/flamingock/examples/inventory/order/OrderController.java b/inventory-orders-service/src/main/java/io/flamingock/examples/inventory/order/OrderController.java new file mode 100644 index 0000000..017002e --- /dev/null +++ b/inventory-orders-service/src/main/java/io/flamingock/examples/inventory/order/OrderController.java @@ -0,0 +1,32 @@ +package io.flamingock.examples.inventory.order; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping("/orders") +public class OrderController { + + private final OrderRepository orderRepository; + + public OrderController(OrderRepository orderRepository) { + this.orderRepository = orderRepository; + } + + @GetMapping + public List getAllOrders() { + return orderRepository.findAll(); + } + + @GetMapping("/{orderId}") + public ResponseEntity getOrder(@PathVariable String orderId) { + return orderRepository.findById(orderId) + .map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } +} diff --git a/inventory-orders-service/src/main/java/io/flamingock/examples/inventory/order/OrderItem.java b/inventory-orders-service/src/main/java/io/flamingock/examples/inventory/order/OrderItem.java new file mode 100644 index 0000000..59bc164 --- /dev/null +++ b/inventory-orders-service/src/main/java/io/flamingock/examples/inventory/order/OrderItem.java @@ -0,0 +1,3 @@ +package io.flamingock.examples.inventory.order; + +public record OrderItem(String productId, int quantity, double price) {} diff --git a/inventory-orders-service/src/main/java/io/flamingock/examples/inventory/order/OrderRepository.java b/inventory-orders-service/src/main/java/io/flamingock/examples/inventory/order/OrderRepository.java new file mode 100644 index 0000000..b9bb064 --- /dev/null +++ b/inventory-orders-service/src/main/java/io/flamingock/examples/inventory/order/OrderRepository.java @@ -0,0 +1,66 @@ +package io.flamingock.examples.inventory.order; + +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.MongoCursor; +import org.bson.Document; +import org.springframework.stereotype.Repository; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import static com.mongodb.client.model.Filters.eq; + +@Repository +public class OrderRepository { + + private static final String DATABASE_NAME = "inventory"; + private static final String COLLECTION_NAME = "orders"; + + private final MongoCollection collection; + + public OrderRepository(MongoClient mongoClient) { + this.collection = mongoClient.getDatabase(DATABASE_NAME).getCollection(COLLECTION_NAME); + } + + public List findAll() { + List orders = new ArrayList<>(); + try (MongoCursor cursor = collection.find().iterator()) { + while (cursor.hasNext()) { + orders.add(toOrder(cursor.next())); + } + } + return orders; + } + + public Optional findById(String orderId) { + Document doc = collection.find(eq("orderId", orderId)).first(); + return Optional.ofNullable(doc).map(this::toOrder); + } + + private Order toOrder(Document doc) { + List items = new ArrayList<>(); + List itemDocs = doc.getList("items", Document.class); + if (itemDocs != null) { + for (Document itemDoc : itemDocs) { + items.add(new OrderItem( + itemDoc.getString("productId"), + itemDoc.getInteger("quantity"), + itemDoc.getDouble("price") + )); + } + } + + return new Order( + doc.getString("orderId"), + doc.getString("customerId"), + items, + doc.getDouble("total"), + doc.getString("status"), + doc.getString("createdAt"), + doc.getString("discountCode"), + doc.getBoolean("discountApplied", false) + ); + } +} diff --git a/inventory-orders-service/src/main/resources/application.yml b/inventory-orders-service/src/main/resources/application.yml new file mode 100644 index 0000000..6e32f5f --- /dev/null +++ b/inventory-orders-service/src/main/resources/application.yml @@ -0,0 +1,6 @@ +spring: + application: + name: inventory-orders-service + +server: + port: 8080 diff --git a/inventory-orders-service/src/test/java/io/flamingock/examples/inventory/SuccessExecutionTest.java b/inventory-orders-service/src/test/java/io/flamingock/examples/inventory/SuccessExecutionTest.java index ac8db19..71191e6 100644 --- a/inventory-orders-service/src/test/java/io/flamingock/examples/inventory/SuccessExecutionTest.java +++ b/inventory-orders-service/src/test/java/io/flamingock/examples/inventory/SuccessExecutionTest.java @@ -50,10 +50,10 @@ import java.nio.file.Path; import java.util.*; -import static io.flamingock.examples.inventory.util.TargetSystems.DATABASE_NAME; -import static io.flamingock.examples.inventory.util.TargetSystems.KAFKA_TARGET_SYSTEM; -import static io.flamingock.examples.inventory.util.TargetSystems.MONGODB_TARGET_SYSTEM; -import static io.flamingock.examples.inventory.util.TargetSystems.FEATURE_FLAG_TARGET_SYSTEM; +import static io.flamingock.examples.inventory.TargetSystems.DATABASE_NAME; +import static io.flamingock.examples.inventory.TargetSystems.KAFKA_TARGET_SYSTEM; +import static io.flamingock.examples.inventory.TargetSystems.MONGODB_TARGET_SYSTEM; +import static io.flamingock.examples.inventory.TargetSystems.FEATURE_FLAG_TARGET_SYSTEM; import static org.junit.jupiter.api.Assertions.*; @Testcontainers @@ -149,9 +149,9 @@ static void beforeAll() throws Exception { static class TestConfig {} private static void runFlamingockMigrations(MongoClient mongoClient, KafkaSchemaManager schemaManager, LaunchDarklyClient launchDarklyClient) { - CommunityAuditStore auditStore = new MongoDBSyncAuditStore(mongoClient, DATABASE_NAME); - MongoDBSyncTargetSystem mongoTarget = new MongoDBSyncTargetSystem(MONGODB_TARGET_SYSTEM, mongoClient, DATABASE_NAME); + CommunityAuditStore auditStore = MongoDBSyncAuditStore.from(mongoTarget); + NonTransactionalTargetSystem kafkaTarget = new NonTransactionalTargetSystem(KAFKA_TARGET_SYSTEM).addDependency(schemaManager); NonTransactionalTargetSystem flagTarget = new NonTransactionalTargetSystem(FEATURE_FLAG_TARGET_SYSTEM).addDependency(launchDarklyClient); @@ -265,4 +265,4 @@ private void verifyChangeExecution(List auditLogs, String changeId) { && "APPLIED".equals(log.getString("state"))); assertTrue(hasApplied, "Change " + changeId + " should have APPLIED entry"); } -} \ No newline at end of file +}