diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 2a2c8ac..6657d8b 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -21,10 +21,6 @@ "label": "KurrentDB API", "onAutoForward": "silent" }, - "27017": { - "label": "MongoDB", - "onAutoForward": "silent" - }, "5432": { "label": "PostgreSQL", "onAutoForward": "silent" diff --git a/commerce/.config/dotnet-tools.json b/commerce/.config/dotnet-tools.json deleted file mode 100644 index f8bfcbf..0000000 --- a/commerce/.config/dotnet-tools.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "version": 1, - "isRoot": true, - "tools": { - "fantomas": { - "version": "7.0.1", - "commands": [ - "fantomas" - ], - "rollForward": false - } - } -} \ No newline at end of file diff --git a/commerce/.gitignore b/commerce/.gitignore deleted file mode 100644 index f01b3d9..0000000 --- a/commerce/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -bin -obj -.idea -*.user -.artifacts \ No newline at end of file diff --git a/commerce/Kurrent.Extensions.Commerce.sln b/commerce/Kurrent.Extensions.Commerce.sln deleted file mode 100644 index ef8bd06..0000000 --- a/commerce/Kurrent.Extensions.Commerce.sln +++ /dev/null @@ -1,34 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.31903.59 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Kurrent.Extensions.Commerce", "Kurrent.Extensions.Commerce\Kurrent.Extensions.Commerce.fsproj", "{271FA701-D4F3-4134-B22A-1AEE91C10C49}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Debug|x64 = Debug|x64 - Debug|x86 = Debug|x86 - Release|Any CPU = Release|Any CPU - Release|x64 = Release|x64 - Release|x86 = Release|x86 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {271FA701-D4F3-4134-B22A-1AEE91C10C49}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {271FA701-D4F3-4134-B22A-1AEE91C10C49}.Debug|Any CPU.Build.0 = Debug|Any CPU - {271FA701-D4F3-4134-B22A-1AEE91C10C49}.Debug|x64.ActiveCfg = Debug|Any CPU - {271FA701-D4F3-4134-B22A-1AEE91C10C49}.Debug|x64.Build.0 = Debug|Any CPU - {271FA701-D4F3-4134-B22A-1AEE91C10C49}.Debug|x86.ActiveCfg = Debug|Any CPU - {271FA701-D4F3-4134-B22A-1AEE91C10C49}.Debug|x86.Build.0 = Debug|Any CPU - {271FA701-D4F3-4134-B22A-1AEE91C10C49}.Release|Any CPU.ActiveCfg = Release|Any CPU - {271FA701-D4F3-4134-B22A-1AEE91C10C49}.Release|Any CPU.Build.0 = Release|Any CPU - {271FA701-D4F3-4134-B22A-1AEE91C10C49}.Release|x64.ActiveCfg = Release|Any CPU - {271FA701-D4F3-4134-B22A-1AEE91C10C49}.Release|x64.Build.0 = Release|Any CPU - {271FA701-D4F3-4134-B22A-1AEE91C10C49}.Release|x86.ActiveCfg = Release|Any CPU - {271FA701-D4F3-4134-B22A-1AEE91C10C49}.Release|x86.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection -EndGlobal diff --git a/commerce/Kurrent.Extensions.Commerce/Configuration.fs b/commerce/Kurrent.Extensions.Commerce/Configuration.fs deleted file mode 100644 index 911ed28..0000000 --- a/commerce/Kurrent.Extensions.Commerce/Configuration.fs +++ /dev/null @@ -1,58 +0,0 @@ -namespace Kurrent.Extensions.Commerce - -open System -open NodaTime - -type PeriodConfiguration = { From: Instant; To: Instant } - -type CountConfiguration = { Minimum: int; Maximum: int } - -type DurationConfiguration = - { Minimum: Duration; Maximum: Duration } - -type ShoppingConfiguration = - { ShoppingPeriod: PeriodConfiguration - CartCount: CountConfiguration - ConcurrentCartCount: CountConfiguration - CartActionCount: CountConfiguration - TimeBetweenCartActions: DurationConfiguration - TimeBetweenCheckoutActions: DurationConfiguration - AbandonCartAfterTime: Duration } - - static member Default = - { ShoppingPeriod = - { From = Instant.FromUtc(2020, 1, 1, 0, 0, 0) - To = Instant.FromDateTimeOffset(DateTimeOffset.UtcNow) } - CartCount = { Minimum = 500; Maximum = 1000 } - ConcurrentCartCount = - { Minimum = 1 - Maximum = Environment.ProcessorCount } - CartActionCount = { Minimum = 1; Maximum = 10 } - TimeBetweenCartActions = - { Minimum = Duration.FromSeconds 5.0 - Maximum = Duration.FromMinutes 15.0 } - TimeBetweenCheckoutActions = - { Minimum = Duration.FromSeconds 30.0 - Maximum = Duration.FromMinutes 2.0 } - AbandonCartAfterTime = Duration.FromHours 1.0 } - -type ProductSource = - | OpenFoodFacts = 0 - | Amazon = 1 - | Walmart = 2 - -type PIMConfiguration = - { ProductCount: CountConfiguration - ProductSource: ProductSource } - - static member Default = - { ProductCount = { Minimum = 1000; Maximum = 5000 } - ProductSource = ProductSource.Amazon } - -type Configuration = - { Shopping: ShoppingConfiguration - PIM: PIMConfiguration } - - static member Default = - { Shopping = ShoppingConfiguration.Default - PIM = PIMConfiguration.Default } diff --git a/commerce/Kurrent.Extensions.Commerce/FakeExtensions.fs b/commerce/Kurrent.Extensions.Commerce/FakeExtensions.fs deleted file mode 100644 index aaed3d4..0000000 --- a/commerce/Kurrent.Extensions.Commerce/FakeExtensions.fs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Bogus - -open Kurrent.Extensions.Commerce - -module FakerExtensions = - type Faker with - member this.FoodProducts() = - let has_context: IHasContext = upcast this - has_context.Context[$"__{(nameof FoodProducts).ToLowerInvariant()}"] :?> FoodProducts diff --git a/commerce/Kurrent.Extensions.Commerce/FileFormat.fs b/commerce/Kurrent.Extensions.Commerce/FileFormat.fs deleted file mode 100644 index a6d2d8c..0000000 --- a/commerce/Kurrent.Extensions.Commerce/FileFormat.fs +++ /dev/null @@ -1,5 +0,0 @@ -namespace Kurrent.Extensions.Commerce - -type FileFormat = - | Json - | Zip diff --git a/commerce/Kurrent.Extensions.Commerce/FoodProducts.fs b/commerce/Kurrent.Extensions.Commerce/FoodProducts.fs deleted file mode 100644 index e10a408..0000000 --- a/commerce/Kurrent.Extensions.Commerce/FoodProducts.fs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Kurrent.Extensions.Commerce - -open Bogus - -type FoodProducts(products: PIM.Product[], weights: float32[]) = - inherit DataSet() - - member this.Product() = - this.Random.WeightedRandom(products, weights) diff --git a/commerce/Kurrent.Extensions.Commerce/Framework/AsyncCommandExtensions.fs b/commerce/Kurrent.Extensions.Commerce/Framework/AsyncCommandExtensions.fs deleted file mode 100644 index f998958..0000000 --- a/commerce/Kurrent.Extensions.Commerce/Framework/AsyncCommandExtensions.fs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Spectre.Console.Cli - -open System.Text.Json -open Microsoft.Extensions.Logging -open Minerals.StringCases - -module AsyncCommandExtensions = - type AsyncCommand<'Settings when 'Settings :> CommandSettings> with - member this.Describe(settings: 'Settings, logger: ILogger) = - logger.LogInformation( - "Executing command '{Command}' with settings {Settings}", - this.GetType().DeclaringType.Name.ToKebabCase(), - JsonSerializer.Serialize(settings) - ) diff --git a/commerce/Kurrent.Extensions.Commerce/Framework/ClockExtensions.fs b/commerce/Kurrent.Extensions.Commerce/Framework/ClockExtensions.fs deleted file mode 100644 index 8f15312..0000000 --- a/commerce/Kurrent.Extensions.Commerce/Framework/ClockExtensions.fs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Kurrent.Extensions.Commerce.Framework - -open NodaTime -open NodaTime.Testing -open Bogus - -module ClockExtensions = - type FakeClock with - member this.AdvanceTimeBetweenActions (faker: Faker) (minimum: Duration) (maximum: Duration) = - let time = - Duration.FromSeconds(faker.Random.Double(minimum.TotalSeconds, maximum.TotalSeconds)) - - this.Advance time diff --git a/commerce/Kurrent.Extensions.Commerce/Framework/ConfiguratorExtensions.fs b/commerce/Kurrent.Extensions.Commerce/Framework/ConfiguratorExtensions.fs deleted file mode 100644 index 3554b18..0000000 --- a/commerce/Kurrent.Extensions.Commerce/Framework/ConfiguratorExtensions.fs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Spectre.Console.Cli - -open System.ComponentModel -open System.Reflection -open Minerals.StringCases - -module ConfiguratorExtensions = - type IConfigurator with - member this.AddCommand<'Command when 'Command :> ICommand and 'Command: not struct>() = - this - .AddCommand<'Command>(typeof<'Command>.DeclaringType.Name.ToKebabCase()) - .WithDescription(typeof<'Command>.GetCustomAttribute().Description) - |> ignore - - this diff --git a/commerce/Kurrent.Extensions.Commerce/Framework/DuckDBConnectionStringBuilder.fs b/commerce/Kurrent.Extensions.Commerce/Framework/DuckDBConnectionStringBuilder.fs deleted file mode 100644 index 903172b..0000000 --- a/commerce/Kurrent.Extensions.Commerce/Framework/DuckDBConnectionStringBuilder.fs +++ /dev/null @@ -1,11 +0,0 @@ -namespace DuckDB.NET.Data - -module Extensions = - type DuckDBConnectionStringBuilder with - member this.Clone() = - let builder = DuckDBConnectionStringBuilder() - - for key in this.Keys do - builder.Add(downcast key, this.Item(downcast key)) - - builder diff --git a/commerce/Kurrent.Extensions.Commerce/Framework/Sql.fs b/commerce/Kurrent.Extensions.Commerce/Framework/Sql.fs deleted file mode 100644 index 52e3ec2..0000000 --- a/commerce/Kurrent.Extensions.Commerce/Framework/Sql.fs +++ /dev/null @@ -1,140 +0,0 @@ -namespace Kurrent.Extensions.Commerce.Framework - -open System -open System.Collections.Generic -open System.IO -open FSharp.Control -open DuckDB.NET.Data -open DuckDB.NET.Data.Extensions -open Microsoft.Extensions.Logging -open Microsoft.Extensions.Logging.Abstractions - -module Sql = - type SqlProps = - private - { Builder: DuckDBConnectionStringBuilder - Logger: ILogger } - - let private sanitize (text: string) = - let sanitized = - text.ReplaceLineEndings(" ").Split(' ', StringSplitOptions.RemoveEmptyEntries) - - String.Join(" ", sanitized) - - let private getAvailableMemoryInGigabytes () = - float (GC.GetGCMemoryInfo().TotalAvailableMemoryBytes) / (1024.0 ** 3) - - let connect (memory_percentage: int) (cpu_percentage: int) = - let max_memory = - int (getAvailableMemoryInGigabytes () * (float memory_percentage / 100.0)) - - let max_cpu = - int (float Environment.ProcessorCount * (float cpu_percentage / 100.0)) - - let builder = DuckDBConnectionStringBuilder(DataSource = ":memory:") - builder.Add("threads", max_cpu) - builder.Add("memory_limit", $"{max_memory}GB") - - { Builder = builder - Logger = NullLogger.Instance } - - let connect_with_defaults () = connect 50 50 - - let log (logger: ILogger) (props: SqlProps) = { props with Logger = logger } - - let query text read (props: SqlProps) = - taskSeq { - let db = props.Builder.Clone() - let temporary_db = Path.GetTempFileName() - File.Delete(temporary_db) // DuckDB expects a database file and an empty file is not a database file according to DuckDB - db.DataSource <- temporary_db - use connection = new DuckDBConnection(db.ConnectionString) - - props.Logger.LogInformation( - "Executing SQL query \"{Query}\" on connection \"{ConnectionString}\"", - sanitize (text), - db.ConnectionString - ) - - try - do! connection.OpenAsync() - use command = connection.CreateCommand() - command.CommandText <- text - command.UseStreamingMode <- true - use reader = command.ExecuteReader() - - if not reader.IsClosed then - while reader.Read() do - yield read reader - - do! connection.CloseAsync() - finally - if File.Exists(db.DataSource) then - File.Delete(db.DataSource) // Clean up the temporary database file - } - - let query_single text read (props: SqlProps) = - query text read props |> TaskSeq.exactlyOne - - let parameterized_query text read (parameters: IReadOnlyDictionary) (props: SqlProps) = - taskSeq { - let db = props.Builder.Clone() - let temporary_db = Path.GetTempFileName() - File.Delete(temporary_db) // DuckDB expects a database file and an empty file is not a database file according to DuckDB - db.DataSource <- temporary_db - use connection = new DuckDBConnection(db.ConnectionString) - - props.Logger.LogInformation( - "Executing parameterized SQL query \"{Query}\" on connection \"{ConnectionString}\"", - sanitize (text), - db.ConnectionString - ) - - try - do! connection.OpenAsync() - use command = connection.CreateCommand() - command.CommandText <- text - command.UseStreamingMode <- true - - for parameter in parameters do - command.Parameters.Add(DuckDBParameter(parameter.Key, parameter.Value)) - |> ignore - - use reader = command.ExecuteReader() - - if not reader.IsClosed then - while reader.Read() do - yield read reader - - do! connection.CloseAsync() - finally - if File.Exists(db.DataSource) then - File.Delete(db.DataSource) // Clean up the temporary database file - } - - let query_scalar text (props: SqlProps) = - task { - let db = props.Builder.Clone() - let temporary_db = Path.GetTempFileName() - File.Delete(temporary_db) // DuckDB expects a database file and an empty file is not a database file according to DuckDB - db.DataSource <- temporary_db - use connection = new DuckDBConnection(db.ConnectionString) - - props.Logger.LogInformation( - "Executing scalar SQL query \"{Query}\" on connection \"{ConnectionString}\"", - sanitize (text), - db.ConnectionString - ) - - try - do! connection.OpenAsync() - use command = connection.CreateCommand() - command.CommandText <- text - command.UseStreamingMode <- true - let! result = command.ExecuteScalarAsync() - do! connection.CloseAsync() - return result - finally - if File.Exists(db.DataSource) then - File.Delete(db.DataSource) // Clean up the temporary database file - } diff --git a/commerce/Kurrent.Extensions.Commerce/GenerateDataSet.fs b/commerce/Kurrent.Extensions.Commerce/GenerateDataSet.fs deleted file mode 100644 index c893fec..0000000 --- a/commerce/Kurrent.Extensions.Commerce/GenerateDataSet.fs +++ /dev/null @@ -1,230 +0,0 @@ -namespace Kurrent.Extensions.Commerce - -open System -open System.ComponentModel -open System.IO -open System.IO.Compression -open System.Text.Json -open System.Text.Json.Serialization -open Bogus -open FSharp.Control -open Microsoft.Extensions.Logging -open NodaTime -open NodaTime.Serialization.SystemTextJson -open NodaTime.Testing -open Spectre.Console.Cli -open Spectre.Console.Cli.AsyncCommandExtensions - -module GenerateDataSet = - type private EventDataRecord = - { Id: Guid - Type: string - ContentType: string - Data: JsonElement } - - type private StreamEventRecord = - { Stream: StreamName - Event: EventDataRecord - DataLength: int64 } - - type private StreamBatchRecord = - { Stream: StreamName - Events: EventDataRecord[] - DataLength: int64 } - - [] - let private max_append_size = 1_000_000 - - let private batch (stream: TaskSeq) = - taskSeq { - use enumerator = stream.GetAsyncEnumerator() - let! moved = enumerator.MoveNextAsync() - - if moved then - let mutable current_stream = enumerator.Current.Stream - let mutable batch_size = enumerator.Current.DataLength - let batch = ResizeArray() - batch.Add enumerator.Current.Event - - while! enumerator.MoveNextAsync() do - // Still the same stream - if enumerator.Current.Stream = current_stream then - // Keep growing the batch until we're over the max append size - if batch_size + enumerator.Current.DataLength < max_append_size then - batch.Add enumerator.Current.Event - batch_size <- batch_size + enumerator.Current.DataLength - else - yield - { Stream = current_stream - Events = batch.ToArray() - DataLength = batch_size } - - batch.Clear() - batch.Add enumerator.Current.Event - batch_size <- enumerator.Current.DataLength - else - yield - { Stream = current_stream - Events = batch.ToArray() - DataLength = batch_size } - - batch.Clear() - batch.Add enumerator.Current.Event - batch_size <- enumerator.Current.DataLength - current_stream <- enumerator.Current.Stream - - // Make sure we yield any residue - if batch.Count > 0 then - yield - { Stream = current_stream - Events = batch.ToArray() - DataLength = batch_size } - } - - type Settings() = - inherit CommandSettings() - - [] - [] - [] - member val ConfigurationFile = "" with get, set - - [] - [] - [] - member val OutputFile = "commerce-data-set.zip" with get, set - - member this.DetectOutputFormat() = - match Path.GetExtension(this.OutputFile).ToLowerInvariant() with - | ".json" -> Json - | ".zip" -> Zip - | _ -> failwith $"The output file '{this.OutputFile}' is neither a JSON or ZIP file." - - [] - type Command(logger: ILogger) = - inherit AsyncCommand() - - let write_output (writer: Utf8JsonWriter) (output: TaskSeq) = - task { - writer.WriteStartArray() - - do! - output - |> TaskSeq.iter (fun record -> - writer.WriteStartObject() - writer.WriteString("stream", StreamName.toString record.Stream) - writer.WritePropertyName("events") - writer.WriteStartArray() - - record.Events - |> Array.iter (fun event -> - writer.WriteStartObject() - writer.WriteString("id", event.Id.ToString()) - writer.WriteString("type", event.Type) - writer.WriteString("content-type", event.ContentType) - writer.WritePropertyName("data") - event.Data.WriteTo writer - //skipping metadata for now - writer.WriteEndObject()) - - writer.WriteEndArray() - writer.WriteEndObject()) - - writer.WriteEndArray() - writer.Flush() - } - - override this.ExecuteAsync(context, settings) = - task { - this.Describe(settings, logger) - - let options = - JsonFSharpOptions - .Default() - .WithUnionUntagged() - .WithUnionUnwrapRecordCases() - .ToJsonSerializerOptions() - .ConfigureForNodaTime(DateTimeZoneProviders.Tzdb) - - options.PropertyNamingPolicy <- JsonNamingPolicy.CamelCase - options.Converters.Add(JsonStringEnumConverter(JsonNamingPolicy.CamelCase)) - - let configuration: Configuration = - match settings.ConfigurationFile with - | "" -> Configuration.Default - | file -> - if File.Exists file then - let json = JsonDocument.Parse(File.ReadAllText file) - JsonSerializer.Deserialize(json.RootElement, options) - else - failwith $"The configuration file '{file}' does not exist." - - let faker = Faker() - - let simulator: ISimulator = ShoppingJourneySimulator(faker) - - do! ProductCatalogBuilder.build faker configuration logger - - let output = - taskSeq { - let cart_count = - faker.Random.Int( - configuration.Shopping.CartCount.Minimum, - configuration.Shopping.CartCount.Maximum - ) - - logger.LogInformation("Generating {CartCount} carts", cart_count) - - let time_between_carts = - Duration.FromTicks( - (configuration.Shopping.ShoppingPeriod.To - - configuration.Shopping.ShoppingPeriod.From) - .TotalTicks - / double cart_count - ) - - logger.LogInformation( - "Time between carts is {Days} {Hours}:{Minutes}:{Seconds}", - time_between_carts.Days, - time_between_carts.Hours, - time_between_carts.Minutes, - time_between_carts.Seconds - ) - - let clock = FakeClock(configuration.Shopping.ShoppingPeriod.From) - - for _ in 1..cart_count do - yield! (simulator.Simulate (FakeClock(clock.GetCurrentInstant())) configuration) - clock.Advance(time_between_carts) - } - |> TaskSeq.map (fun (stream, event) -> - let encoded = JsonSerializer.SerializeToUtf8Bytes(event, options) - let json = JsonDocument.Parse(encoded) - - { Stream = stream - Event = - { Id = Guid.NewGuid() - Type = event.ToEventType() - ContentType = "application/json" - Data = json.RootElement } - DataLength = encoded.Length }) - |> batch - - match settings.DetectOutputFormat() with - | Zip -> - logger.LogInformation("Writing output to {ZipFile}", settings.OutputFile) - - use zip_file = - new ZipArchive(File.Create(settings.OutputFile), ZipArchiveMode.Create, false) - - use entry_stream = zip_file.CreateEntry("data.json").Open() - use writer = new Utf8JsonWriter(entry_stream, JsonWriterOptions(Indented = true)) - do! write_output writer output - | Json -> - logger.LogInformation("Writing output to {JsonFile}", settings.OutputFile) - use output_file = File.Create(settings.OutputFile) - use writer = new Utf8JsonWriter(output_file, JsonWriterOptions(Indented = true)) - do! write_output writer output - - return 0 - } diff --git a/commerce/Kurrent.Extensions.Commerce/GetDefaultConfiguration.fs b/commerce/Kurrent.Extensions.Commerce/GetDefaultConfiguration.fs deleted file mode 100644 index b5450df..0000000 --- a/commerce/Kurrent.Extensions.Commerce/GetDefaultConfiguration.fs +++ /dev/null @@ -1,47 +0,0 @@ -namespace Kurrent.Extensions.Commerce - -open System.ComponentModel -open System.IO -open System.Text.Json -open System.Text.Json.Serialization -open Microsoft.Extensions.Logging -open NodaTime -open NodaTime.Serialization.SystemTextJson -open Spectre.Console.Cli -open Spectre.Console.Cli.AsyncCommandExtensions - -module GetDefaultConfiguration = - type Settings() = - inherit CommandSettings() - - [] - [")>] - member val OutputFile = "" with get, set - - [] - type Command(logger: ILogger) = - inherit AsyncCommand() - - override this.ExecuteAsync(context, settings) = - task { - this.Describe(settings, logger) - - let options = - JsonFSharpOptions - .Default() - .WithUnionUntagged() - .WithUnionUnwrapRecordCases() - .ToJsonSerializerOptions() - .ConfigureForNodaTime(DateTimeZoneProviders.Tzdb) - - options.PropertyNamingPolicy <- JsonNamingPolicy.CamelCase - options.Converters.Add(JsonStringEnumConverter(JsonNamingPolicy.CamelCase)) - - logger.LogInformation("Writing output to {JsonFile}", settings.OutputFile) - - let json = JsonSerializer.Serialize(Configuration.Default, options) - - File.WriteAllText(settings.OutputFile, json) - - return 0 - } diff --git a/commerce/Kurrent.Extensions.Commerce/ISimulator.fs b/commerce/Kurrent.Extensions.Commerce/ISimulator.fs deleted file mode 100644 index a0954bf..0000000 --- a/commerce/Kurrent.Extensions.Commerce/ISimulator.fs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Kurrent.Extensions.Commerce - -open FSharp.Control -open NodaTime.Testing - -type ISimulator<'Event> = - abstract member Simulate: FakeClock -> Configuration -> TaskSeq diff --git a/commerce/Kurrent.Extensions.Commerce/Kurrent.Extensions.Commerce.fsproj b/commerce/Kurrent.Extensions.Commerce/Kurrent.Extensions.Commerce.fsproj deleted file mode 100644 index 782a649..0000000 --- a/commerce/Kurrent.Extensions.Commerce/Kurrent.Extensions.Commerce.fsproj +++ /dev/null @@ -1,49 +0,0 @@ - - - - Exe - net9.0 - edb-commerce - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/commerce/Kurrent.Extensions.Commerce/LiveDataSet.fs b/commerce/Kurrent.Extensions.Commerce/LiveDataSet.fs deleted file mode 100644 index 7513346..0000000 --- a/commerce/Kurrent.Extensions.Commerce/LiveDataSet.fs +++ /dev/null @@ -1,175 +0,0 @@ -namespace Kurrent.Extensions.Commerce - -open System -open System.Collections.Concurrent -open System.ComponentModel -open System.IO -open System.IO.Compression -open System.Text.Json -open System.Text.Json.Serialization -open System.Threading -open System.Threading.Tasks -open Bogus -open EventStore.Client -open FSharp.Control -open Microsoft.Extensions.Logging -open NodaTime -open NodaTime.Serialization.SystemTextJson -open NodaTime.Testing -open Spectre.Console.Cli -open Spectre.Console.Cli.AsyncCommandExtensions - -module LiveDataSet = - type Settings() = - inherit CommandSettings() - - [] - [] - [] - member val ConfigurationFile = "" with get, set - - [] - [] - [] - member val ConnectionString = "esdb://localhost:2113?tls=false" with get, set - - [] - type Command(logger: ILogger) = - inherit AsyncCommand() - - let append - (client: EventStoreClient) - (options: JsonSerializerOptions) - (revisions: ConcurrentDictionary) - (stream: StreamName) - (events: Shopping.Event array) - = - task { - let expected = - match revisions.TryGetValue stream with - | true, revision -> revision - | false, _ -> StreamRevision.None - - let data = - events - |> Array.map (fun event -> - EventData( - Uuid.NewUuid(), - event.ToEventType(), - ReadOnlyMemory(JsonSerializer.SerializeToUtf8Bytes(event, options)) - )) - - let! append_result = client.AppendToStreamAsync(StreamName.toString stream, expected, data) - revisions[stream] <- append_result.NextExpectedStreamRevision - } - - override this.ExecuteAsync(_, settings) = - task { - this.Describe(settings, logger) - - let options = - JsonFSharpOptions - .Default() - .WithUnionUntagged() - .WithUnionUnwrapRecordCases() - .ToJsonSerializerOptions() - .ConfigureForNodaTime(DateTimeZoneProviders.Tzdb) - - options.PropertyNamingPolicy <- JsonNamingPolicy.CamelCase - options.Converters.Add(JsonStringEnumConverter(JsonNamingPolicy.CamelCase)) - - let configuration: Configuration = - match settings.ConfigurationFile with - | "" -> Configuration.Default - | file -> - if File.Exists file then - let json = JsonDocument.Parse(File.ReadAllText file) - JsonSerializer.Deserialize(json.RootElement, options) - else - failwith $"The configuration file '{file}' does not exist." - - let clock = SystemClock.Instance - - let faker = Faker() - - do! ProductCatalogBuilder.build faker configuration logger - - let client_settings = EventStoreClientSettings.Create settings.ConnectionString - client_settings.OperationOptions.ThrowOnAppendFailure <- false - use client = new EventStoreClient(client_settings) - - let revisions = ConcurrentDictionary() - - let cart_count = - faker.Random.Int(configuration.Shopping.CartCount.Minimum, configuration.Shopping.CartCount.Maximum) - - logger.LogInformation("Generating {CartCount} carts", cart_count) - - let concurrent_cart_count = - faker.Random.Int( - configuration.Shopping.ConcurrentCartCount.Minimum, - configuration.Shopping.ConcurrentCartCount.Maximum - ) - - logger.LogInformation("With {ConcurrencyCount} carts concurrently", concurrent_cart_count) - - let concurrency_options = - ParallelOptions(MaxDegreeOfParallelism = concurrent_cart_count) - - let initial_delay_in_seconds = - int ( - (double configuration.Shopping.CartActionCount.Minimum) - * configuration.Shopping.TimeBetweenCartActions.Minimum.TotalSeconds - ) - - let simulator: ISimulator = ShoppingJourneySimulator(faker) - - do! - Parallel.ForAsync( - 0, - cart_count, - concurrency_options, - fun (cart: int) (ct: CancellationToken) -> - task { - let initial_delay = - Duration.FromSeconds(double (faker.Random.Int(0, initial_delay_in_seconds))) - - do! - simulator.Simulate - (FakeClock(clock.GetCurrentInstant().Plus(initial_delay))) - configuration - |> TaskSeq.iterAsync (fun (stream, event) -> - task { - let until = - Instant.FromDateTimeOffset( - match event with - | Shopping.VisitorStartedShopping e -> e.At - | Shopping.CartShopperGotIdentified e -> e.At - | Shopping.CustomerStartedShopping e -> e.At - | Shopping.ItemGotAddedToCart e -> e.At - | Shopping.ItemGotRemovedFromCart e -> e.At - | Shopping.CartGotCheckedOut e -> e.At - | Shopping.CartGotAbandoned e -> e.At - | Shopping.CheckoutStarted e -> e.At - | Shopping.ShippingInformationCollected e -> e.At - | Shopping.ShippingMethodSelected e -> e.At - | Shopping.ShippingCostCalculated e -> e.At - | Shopping.BillingInformationCollected e -> e.At - | Shopping.BillingInformationCopiedFromShippingInformation e -> - e.At - | Shopping.PaymentMethodSelected e -> e.At - | Shopping.CheckoutCompleted e -> e.At - | Shopping.OrderPlaced e -> e.At - ) - - if until > clock.GetCurrentInstant() then - do! Task.Delay((until - clock.GetCurrentInstant()).ToTimeSpan(), ct) - - do! append client options revisions stream [| event |] - }) - } - |> ValueTask - ) - - return 0 - } diff --git a/commerce/Kurrent.Extensions.Commerce/PIM.fs b/commerce/Kurrent.Extensions.Commerce/PIM.fs deleted file mode 100644 index d93c29a..0000000 --- a/commerce/Kurrent.Extensions.Commerce/PIM.fs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Kurrent.Extensions.Commerce - -[] -module PIM = - type Product = - { Id: string - Name: string - Price: string - TaxRate: decimal } - - type WeightedProduct = { Weight: float32; Product: Product } diff --git a/commerce/Kurrent.Extensions.Commerce/ProductCatalogBuilder.fs b/commerce/Kurrent.Extensions.Commerce/ProductCatalogBuilder.fs deleted file mode 100644 index 7cdefd4..0000000 --- a/commerce/Kurrent.Extensions.Commerce/ProductCatalogBuilder.fs +++ /dev/null @@ -1,142 +0,0 @@ -namespace Kurrent.Extensions.Commerce - -open System.IO -open Bogus -open Flurl -open Flurl.Http -open FSharp.Control -open Kurrent.Extensions.Commerce.Framework -open Microsoft.Extensions.Logging -open NodaTime.Testing - -module ProductCatalogBuilder = - let private download_open_food_facts_data () = - task { - let url = - Url( - "https://huggingface.co/datasets/openfoodfacts/product-database/resolve/main/food.parquet?download=true" - ) - - use! response_stream = url.GetStreamAsync() - use file_stream = File.Create("open_food_facts.parquet") - do! response_stream.CopyToAsync(file_stream) - } - - let private download_amazon_data () = - task { - let url = - Url("https://github.com/luminati-io/eCommerce-dataset-samples/raw/refs/heads/main/amazon-products.csv") - - use! response_stream = url.GetStreamAsync() - use file_stream = File.Create("amazon.csv") - do! response_stream.CopyToAsync(file_stream) - } - - let private download_walmart_data () = - task { - let url = - Url("https://github.com/luminati-io/eCommerce-dataset-samples/raw/refs/heads/main/walmart-products.csv") - - use! response_stream = url.GetStreamAsync() - use file_stream = File.Create("walmart.csv") - do! response_stream.CopyToAsync(file_stream) - } - - let build (faker: Faker) (configuration: Configuration) (log: ILogger) = - task { - // Ensure the product data is downloaded - match configuration.PIM.ProductSource with - | ProductSource.OpenFoodFacts -> - if not (File.Exists "open_food_facts.parquet") then - log.LogInformation("Downloading Open Food Facts product data ... this can take some time") - do! download_open_food_facts_data () - | ProductSource.Amazon -> - if not (File.Exists "amazon.csv") then - log.LogInformation("Downloading Amazon product data ... this can take some time") - do! download_amazon_data () - | ProductSource.Walmart -> - if not (File.Exists "walmart.csv") then - log.LogInformation("Downloading Walmart product data ... this can take some time") - do! download_walmart_data () - | _ -> () - - let product_count = - faker.Random.Int(configuration.PIM.ProductCount.Minimum, configuration.PIM.ProductCount.Maximum) - - log.LogInformation("Generating {ProductCount} products", product_count) - - let! weighted_products = - match configuration.PIM.ProductSource with - | ProductSource.OpenFoodFacts -> - Sql.connect_with_defaults () - |> Sql.query - $"SELECT DISTINCT(product_name[1].text) FROM read_parquet('open_food_facts.parquet') LIMIT {product_count}" - _.GetString(0) - |> TaskSeq.mapi (fun index product_name -> - { Weight = (float32 index) - Product = - { Id = faker.Commerce.Ean13() - Name = product_name - Price = faker.Commerce.Price(0.01m, 1000.00m, 2, "USD") - TaxRate = faker.Random.ArrayElement [| 0.06m; 0.12m; 0.21m |] } } - : PIM.WeightedProduct) - |> TaskSeq.toArrayAsync - | ProductSource.Amazon -> - Sql.connect_with_defaults () - |> Sql.query - $"SELECT DISTINCT(title) FROM read_csv('amazon.csv') LIMIT {product_count}" - _.GetString(0) - |> TaskSeq.mapi (fun index product_name -> - { Weight = (float32 index) - Product = - { Id = faker.Commerce.Ean13() - Name = product_name - Price = faker.Commerce.Price(0.01m, 1000.00m, 2, "USD") - TaxRate = faker.Random.ArrayElement [| 0.06m; 0.12m; 0.21m |] } } - : PIM.WeightedProduct) - |> TaskSeq.toArrayAsync - | ProductSource.Walmart -> - Sql.connect_with_defaults () - |> Sql.query - $"SELECT DISTINCT(product_name) FROM read_csv('walmart.csv') LIMIT {product_count}" - _.GetString(0) - |> TaskSeq.mapi (fun index product_name -> - { Weight = (float32 index) - Product = - { Id = faker.Commerce.Ean13() - Name = product_name - Price = faker.Commerce.Price(0.01m, 1000.00m, 2, "USD") - TaxRate = faker.Random.ArrayElement [| 0.06m; 0.12m; 0.21m |] } } - : PIM.WeightedProduct) - |> TaskSeq.toArrayAsync - | _ -> - [ for index in 1..product_count do - let product_name = faker.Commerce.ProductName() - - { Weight = (float32 index) - Product = - { Id = faker.Commerce.Ean13() - Name = product_name - Price = faker.Commerce.Price(0.01m, 1000.00m, 2, "USD") - TaxRate = faker.Random.ArrayElement [| 0.06m; 0.12m; 0.21m |] } } - : PIM.WeightedProduct ] - |> TaskSeq.ofList - |> TaskSeq.toArrayAsync - - let total_weight = weighted_products |> Array.sumBy _.Weight - - let normalized_products = - weighted_products - |> Array.map (fun weighted_product -> - { weighted_product with - Weight = weighted_product.Weight / total_weight }) - - let products = normalized_products |> Array.map _.Product - let weights = normalized_products |> Array.map _.Weight - - let dataset = FoodProducts(products, weights) - let has_randomizer: IHasRandomizer = upcast faker - has_randomizer.GetNotifier().Flow(dataset) |> ignore - let has_context: IHasContext = upcast faker - has_context.Context[$"__{(nameof FoodProducts).ToLowerInvariant()}"] <- dataset - } diff --git a/commerce/Kurrent.Extensions.Commerce/Program.fs b/commerce/Kurrent.Extensions.Commerce/Program.fs deleted file mode 100644 index 8489638..0000000 --- a/commerce/Kurrent.Extensions.Commerce/Program.fs +++ /dev/null @@ -1,120 +0,0 @@ -namespace Kurrent.Extensions.Commerce - -open System -open System.Reflection -open System.Text.Json -open System.Text.Json.Serialization -open Kurrent.Extensions.Commerce.Framework -open EventStore.Client -open FSharp.Control -open Microsoft.Extensions.DependencyInjection -open Microsoft.Extensions.Logging -open Microsoft.Extensions.Logging.Console -open NodaTime -open Spectre.Console -open Spectre.Console.Cli -open Spectre.Console.Cli.ConfiguratorExtensions - -module Program = - type private TypeResolver(provider: IServiceProvider) = - interface ITypeResolver with - member this.Resolve(clrType: Type) = provider.GetService(clrType) - - type private TypeRegistrar(builder: IServiceCollection) = - interface ITypeRegistrar with - member this.Build() = - TypeResolver(builder.BuildServiceProvider()) :> ITypeResolver - - member this.Register(service: Type, implementation: Type) = - builder.AddSingleton(service, implementation) |> ignore - - member this.RegisterInstance(service: Type, implementation: obj) = - builder.AddSingleton(service, implementation) |> ignore - - member this.RegisterLazy(service: Type, func: Func) = - builder.AddSingleton(service, fun _ -> func.Invoke()) |> ignore - - [] - let main args = - let services = ServiceCollection() - - services.AddLogging(fun c -> - c.AddSimpleConsole(fun o -> - o.IncludeScopes <- false - o.SingleLine <- true - o.TimestampFormat <- "HH:mm:ss " - o.ColorBehavior <- LoggerColorBehavior.Enabled - o.UseUtcTimestamp <- true) - |> ignore) - |> ignore - - services.AddSingleton(fun sp -> sp.GetRequiredService().CreateLogger("edb-commerce")) - |> ignore - - let app = CommandApp(TypeRegistrar(services)) - - app.Configure(fun config -> - config - .SetApplicationName("edb-commerce") - .SetApplicationVersion(Assembly.GetEntryAssembly().GetName().Version.ToString()) - .PropagateExceptions() - // Note that commands: - // - must live in an F# module - // - the name of the F# module will become the command name in kebab case (e.g. name-of-the-module) - // - must be attributed with [] for the description to be picked up - // Please add commands in the order you want them to appear in the help text (mostly alphabetical) - .AddCommand() - .AddCommand() - .AddCommand() - .AddCommand() - |> ignore) - - task { - try - return! app.RunAsync(args) - with error -> - AnsiConsole.WriteException(error, ExceptionFormats.ShortenEverything) - return -1 - } - |> Async.AwaitTask - |> Async.RunSynchronously - -// task { -// let settings = -// EventStoreClientSettings.Create "esdb://admin:changeit@localhost:2113?tls=true&tlsVerifyCert=false" -// -// settings.OperationOptions.ThrowOnAppendFailure <- false -// use client = new EventStoreClient(settings) -// -// let options = -// JsonFSharpOptions.Default().WithUnionUntagged().WithUnionUnwrapRecordCases().ToJsonSerializerOptions() -// -// options.PropertyNamingPolicy <- JsonNamingPolicy.CamelCase -// -// let configuration: ShoppingSimulator.Configuration = -// { ShoppingPeriod = { From = Instant.FromUtc(2020, 1, 1, 0, 0, 0); To = Instant.FromDateTimeOffset(DateTimeOffset.UtcNow) } -// CartCount = 1000 -// CartActionCount = { Minimum = 1; Maximum = 10 } -// TimeBetweenCartActions = -// { Minimum = Duration.FromSeconds 5.0 -// Maximum = Duration.FromMinutes 15.0 } -// AbandonCartAfterTime = Duration.FromHours 1.0 } -// -// //let clock = SystemClock.Instance -// -// do! -// ShoppingSimulator.simulate configuration -// |> TaskSeq.map (fun (stream, event) -> -// stream, -// EventData( -// Uuid.NewUuid(), -// event.ToEventName(), -// ReadOnlyMemory(JsonSerializer.SerializeToUtf8Bytes(event, options)) -// )) -// |> TaskSeq.batch -// |> KurrentDB.seed client -// -// return 0 -// } -// |> Async.AwaitTask -// |> Async.RunSynchronously diff --git a/commerce/Kurrent.Extensions.Commerce/SeedDataSet.fs b/commerce/Kurrent.Extensions.Commerce/SeedDataSet.fs deleted file mode 100644 index 7976889..0000000 --- a/commerce/Kurrent.Extensions.Commerce/SeedDataSet.fs +++ /dev/null @@ -1,129 +0,0 @@ -namespace Kurrent.Extensions.Commerce - -open System -open System.Collections.Generic -open System.ComponentModel -open System.IO -open System.IO.Compression -open System.Text.Json -open System.Text.Json.Serialization -open EventStore.Client -open FSharp.Control -open Microsoft.Extensions.Logging -open NodaTime -open NodaTime.Serialization.SystemTextJson -open Spectre.Console.Cli -open Spectre.Console.Cli.AsyncCommandExtensions - -module SeedDataSet = - type StreamEventRecord = - { [] - Id: Guid - [] - Type: string - [] - ContentType: string - [] - Data: JsonElement } - - type StreamBatchRecord = - { [] - Stream: string - [] - Events: StreamEventRecord[] } - - type Settings() = - inherit CommandSettings() - - [] - [")>] - member val InputPath = "" with get, set - - [] - [] - [] - member val ConnectionString = "esdb://localhost:2113?tls=false" with get, set - - member this.EnsureInputPath() = - if not (File.Exists(this.InputPath)) then - failwith $"The input path '{this.InputPath}' does not exist." - - member this.DetectInputFormat() = - match Path.GetExtension(this.InputPath).ToLowerInvariant() with - | ".zip" -> Zip - | ".json" -> Json - | _ -> failwith $"The input path '{this.InputPath}' is neither a JSON or ZIP file." - - [] - type Command(logger: ILogger) = - inherit AsyncCommand() - - let seed (client: EventStoreClient) (events: TaskSeq) = - task { - let revisions = Dictionary() - - do! - events - |> TaskSeq.iterAsync (fun record -> - task { - let expected = - match revisions.TryGetValue record.Stream with - | true, revision -> revision - | false, _ -> StreamRevision.None - - let data = - record.Events - |> Array.map (fun event -> - EventData( - Uuid.FromGuid event.Id, - event.Type, - JsonSerializer.SerializeToUtf8Bytes(event.Data) - )) - - let! append_result = client.AppendToStreamAsync(record.Stream, expected, data) - - revisions[record.Stream] <- append_result.NextExpectedStreamRevision - }) - } - - override this.ExecuteAsync(context: CommandContext, settings: Settings) = - task { - settings.EnsureInputPath() - - this.Describe(settings, logger) - - let client_settings = EventStoreClientSettings.Create settings.ConnectionString - client_settings.OperationOptions.ThrowOnAppendFailure <- false - use client = new EventStoreClient(client_settings) - - let options = - JsonFSharpOptions - .Default() - .WithUnionUntagged() - .WithUnionUnwrapRecordCases() - .ToJsonSerializerOptions() - .ConfigureForNodaTime(DateTimeZoneProviders.Tzdb) - - options.PropertyNamingPolicy <- JsonNamingPolicy.CamelCase - options.Converters.Add(JsonStringEnumConverter(JsonNamingPolicy.CamelCase)) - - match settings.DetectInputFormat() with - | Json -> - use stream = File.OpenRead(settings.InputPath) - - let data = - JsonSerializer.DeserializeAsyncEnumerable(stream, options) - - do! seed client data - | Zip -> - use archive = new ZipArchive(File.OpenRead(settings.InputPath), ZipArchiveMode.Read) - let entry = archive.Entries[0] - use stream = entry.Open() - - let data = - JsonSerializer.DeserializeAsyncEnumerable(stream, options) - - do! seed client data - - return 0 - } diff --git a/commerce/Kurrent.Extensions.Commerce/Shopping.fs b/commerce/Kurrent.Extensions.Commerce/Shopping.fs deleted file mode 100644 index 335f971..0000000 --- a/commerce/Kurrent.Extensions.Commerce/Shopping.fs +++ /dev/null @@ -1,208 +0,0 @@ -namespace Kurrent.Extensions.Commerce - -open System -open System.ComponentModel -open Minerals.StringCases - -[] -module Shopping = - module Cart = - [] - type VisitorStartedShopping = { CartId: string; At: DateTimeOffset } - - [] - type CartShopperGotIdentified = - { CartId: string - CustomerId: string - At: DateTimeOffset } - - [] - type CustomerStartedShopping = - { CartId: string - CustomerId: string - At: DateTimeOffset } - - [] - type ItemGotAddedToCart = - { CartId: string - ProductId: string - ProductName: string - Quantity: int - PricePerUnit: string - TaxRate: decimal - At: DateTimeOffset } - - [] - type ItemGotRemovedFromCart = - { CartId: string - ProductId: string - Quantity: int - At: DateTimeOffset } - - [] - type CartGotCheckedOut = - { CartId: string - OrderId: string - At: DateTimeOffset } - - [] - type CartGotAbandoned = - { CartId: string - AfterBeingIdleFor: TimeSpan - At: DateTimeOffset } - - module Checkout = - [] - type CheckoutStarted = { Cart: string; At: DateTimeOffset } - - type Recipient = - { Title: string - FullName: string - EmailAddress: string - PhoneNumber: string } - - type Address = { Country: string; Lines: string list } - - [] - type ShippingInformationCollected = - { Cart: string - Recipient: Recipient - Address: Address - Instructions: string - At: DateTimeOffset } - - type ShippingMethod = - | Standard = 0 - | Express = 1 - | Overnight = 2 - | SameDay = 3 - - [] - type ShippingMethodSelected = - { Cart: string - Method: ShippingMethod - At: DateTimeOffset } - - [] - type ShippingCostCalculated = - { Cart: string - ForMethod: ShippingMethod - Cost: string - At: DateTimeOffset } - - type PaymentMethod = - | CreditCard = 0 - | DebitCard = 1 - | WireTransfer = 2 - - [] - type BillingInformationCollected = - { Cart: string - Recipient: Recipient - Address: Address - At: DateTimeOffset } - - [] - type BillingInformationCopiedFromShippingInformation = - { Cart: string - Recipient: Recipient - Address: Address - At: DateTimeOffset } - - [] - type PaymentMethodSelected = - { Cart: string - Method: PaymentMethod - At: DateTimeOffset } - - [] - type CheckoutCompleted = - { Cart: string - OrderId: string - At: DateTimeOffset } - - module OrderFulfillment = - type OrderLineItem = - { ProductId: string - ProductName: string - Quantity: int - PricePerUnit: string - TaxRate: decimal } - - type OrderRecipient = - { Title: string - FullName: string - EmailAddress: string - PhoneNumber: string } - - type Address = { Country: string; Lines: string list } - - type OrderShippingMethod = - | Standard = 0 - | Express = 1 - | Overnight = 2 - | SameDay = 3 - - type OrderPaymentMethod = - | CreditCard = 0 - | DebitCard = 1 - | WireTransfer = 2 - - type OrderShippingInformation = - { Recipient: OrderRecipient - Address: Address - Instructions: string - Method: OrderShippingMethod } - - type OrderBillingInformation = - { Recipient: OrderRecipient - Address: Address - PaymentMethod: OrderPaymentMethod } - - [] - type OrderPlaced = - { OrderId: string - CustomerId: string - CheckoutOfCart: string - LineItems: OrderLineItem list - Shipping: OrderShippingInformation - Billing: OrderBillingInformation - At: DateTimeOffset } - - type Event = - | VisitorStartedShopping of Cart.VisitorStartedShopping - | CartShopperGotIdentified of Cart.CartShopperGotIdentified - | CustomerStartedShopping of Cart.CustomerStartedShopping - | ItemGotAddedToCart of Cart.ItemGotAddedToCart - | ItemGotRemovedFromCart of Cart.ItemGotRemovedFromCart - | CartGotCheckedOut of Cart.CartGotCheckedOut - | CartGotAbandoned of Cart.CartGotAbandoned - | CheckoutStarted of Checkout.CheckoutStarted - | ShippingInformationCollected of Checkout.ShippingInformationCollected - | ShippingMethodSelected of Checkout.ShippingMethodSelected - | ShippingCostCalculated of Checkout.ShippingCostCalculated - | BillingInformationCollected of Checkout.BillingInformationCollected - | BillingInformationCopiedFromShippingInformation of Checkout.BillingInformationCopiedFromShippingInformation - | PaymentMethodSelected of Checkout.PaymentMethodSelected - | CheckoutCompleted of Checkout.CheckoutCompleted - | OrderPlaced of OrderFulfillment.OrderPlaced - - member this.ToEventType() = - match this with - | VisitorStartedShopping _ -> nameof(VisitorStartedShopping).ToKebabCase() - | CartShopperGotIdentified _ -> nameof(CartShopperGotIdentified).ToKebabCase() - | CustomerStartedShopping _ -> nameof(CustomerStartedShopping).ToKebabCase() - | ItemGotAddedToCart _ -> nameof(ItemGotAddedToCart).ToKebabCase() - | ItemGotRemovedFromCart _ -> nameof(ItemGotRemovedFromCart).ToKebabCase() - | CartGotCheckedOut _ -> nameof(CartGotCheckedOut).ToKebabCase() - | CartGotAbandoned _ -> nameof(CartGotAbandoned).ToKebabCase() - | CheckoutStarted _ -> nameof(CheckoutStarted).ToKebabCase() - | ShippingInformationCollected _ -> nameof(ShippingInformationCollected).ToKebabCase() - | ShippingMethodSelected _ -> nameof(ShippingMethodSelected).ToKebabCase() - | ShippingCostCalculated _ -> nameof(ShippingCostCalculated).ToKebabCase() - | BillingInformationCollected _ -> nameof(BillingInformationCollected).ToKebabCase() - | BillingInformationCopiedFromShippingInformation _ -> - nameof(BillingInformationCopiedFromShippingInformation).ToKebabCase() - | PaymentMethodSelected _ -> nameof(PaymentMethodSelected).ToKebabCase() - | CheckoutCompleted _ -> nameof(CheckoutCompleted).ToKebabCase() - | OrderPlaced _ -> nameof(OrderPlaced).ToKebabCase() diff --git a/commerce/Kurrent.Extensions.Commerce/ShoppingJourneySimulator.fs b/commerce/Kurrent.Extensions.Commerce/ShoppingJourneySimulator.fs deleted file mode 100644 index 28a967b..0000000 --- a/commerce/Kurrent.Extensions.Commerce/ShoppingJourneySimulator.fs +++ /dev/null @@ -1,311 +0,0 @@ -namespace Kurrent.Extensions.Commerce - -open System -open Bogus -open Bogus.FakerExtensions -open FSharp.Control -open Kurrent.Extensions.Commerce.Framework.ClockExtensions - -type ShoppingJourneySimulator(faker: Faker) = - let generate_customer_id () = $"customer-%d{faker.Random.Int(0)}" - - let generate_cart_id () = $"cart-{Guid.NewGuid():N}" - - let generate_order_id () = $"order-{Guid.NewGuid():N}" - - interface ISimulator with - member this.Simulate clock configuration = - taskSeq { - let customer_id = generate_customer_id () - let cart_id = generate_cart_id () - let order_id = generate_order_id () - let cart_stream = StreamName.ofString cart_id - let order_stream = StreamName.ofString order_id - let checkout_stream = StreamName.ofString $"checkout-for-{cart_id}" - let mutable shopper_identified = false - let mutable cart_version = 0L - - if faker.Random.Bool() then - yield - cart_stream, - Shopping.CustomerStartedShopping - { CartId = cart_id - CustomerId = customer_id - At = clock.GetCurrentInstant().ToDateTimeOffset() } - - cart_version <- cart_version + 1L - shopper_identified <- true - - else - yield - cart_stream, - Shopping.VisitorStartedShopping - { CartId = cart_id - At = clock.GetCurrentInstant().ToDateTimeOffset() } - - cart_version <- cart_version + 1L - - clock.AdvanceTimeBetweenActions - faker - configuration.Shopping.TimeBetweenCartActions.Minimum - configuration.Shopping.TimeBetweenCartActions.Maximum - - let products_in_cart = ResizeArray() - - for _ in - [ 1 .. faker.Random.Int( - configuration.Shopping.CartActionCount.Minimum, - configuration.Shopping.CartActionCount.Maximum - ) ] do - - if products_in_cart.Count > 1 && faker.Random.Bool() then - let remove_at = faker.Random.Int(0, products_in_cart.Count - 1) - let product_in_cart = products_in_cart[remove_at] - products_in_cart.RemoveAt remove_at - - yield - cart_stream, - Shopping.ItemGotRemovedFromCart - { CartId = cart_id - ProductId = product_in_cart.ProductId - Quantity = product_in_cart.Quantity - At = clock.GetCurrentInstant().ToDateTimeOffset() } - - else - let selected_product = faker.FoodProducts().Product() - - let quantity = faker.Random.Int(1, 5) - - products_in_cart.Add( - { ProductId = selected_product.Id - ProductName = selected_product.Name - Quantity = quantity - PricePerUnit = selected_product.Price - TaxRate = selected_product.TaxRate } - ) - - yield - cart_stream, - Shopping.ItemGotAddedToCart - { CartId = cart_id - ProductId = selected_product.Id - ProductName = selected_product.Name - Quantity = quantity - PricePerUnit = selected_product.Price - TaxRate = selected_product.TaxRate - At = clock.GetCurrentInstant().ToDateTimeOffset() } - - cart_version <- cart_version + 1L - - clock.AdvanceTimeBetweenActions - faker - configuration.Shopping.TimeBetweenCartActions.Minimum - configuration.Shopping.TimeBetweenCartActions.Maximum - - if not shopper_identified && faker.Random.Bool() then - yield - cart_stream, - Shopping.CartShopperGotIdentified - { CartId = cart_id - CustomerId = customer_id - At = clock.GetCurrentInstant().ToDateTimeOffset() } - - cart_version <- cart_version + 1L - - clock.AdvanceTimeBetweenActions - faker - configuration.Shopping.TimeBetweenCartActions.Minimum - configuration.Shopping.TimeBetweenCartActions.Maximum - - if faker.Random.Bool() then - yield - checkout_stream, - Shopping.CheckoutStarted - { Cart = $"{cart_id}@{cart_version}" - At = clock.GetCurrentInstant().ToDateTimeOffset() } - - let recipient: Shopping.Checkout.Recipient = - { Title = faker.Name.Prefix() - FullName = faker.Person.FullName - EmailAddress = faker.Person.Email - PhoneNumber = faker.Phone.PhoneNumber() } - - let address: Shopping.Checkout.Address = - { Lines = - [ yield faker.Address.StreetName() + " " + faker.Address.BuildingNumber() - yield faker.Address.ZipCode() + " " + faker.Address.City() - yield faker.Address.County() ] - Country = faker.Address.CountryCode() } - - clock.AdvanceTimeBetweenActions - faker - configuration.Shopping.TimeBetweenCheckoutActions.Minimum - configuration.Shopping.TimeBetweenCheckoutActions.Maximum - - yield - checkout_stream, - Shopping.ShippingInformationCollected - { Cart = $"{cart_id}@{cart_version}" - Recipient = recipient - Address = address - Instructions = faker.Lorem.Lines(2) - At = clock.GetCurrentInstant().ToDateTimeOffset() } - - clock.AdvanceTimeBetweenActions - faker - configuration.Shopping.TimeBetweenCheckoutActions.Minimum - configuration.Shopping.TimeBetweenCheckoutActions.Maximum - - let shipping_method = faker.PickRandom() - - let mutable billing_recipient: Shopping.Checkout.Recipient = - { Title = faker.Name.Prefix() - FullName = faker.Person.FullName - EmailAddress = faker.Person.Email - PhoneNumber = faker.Phone.PhoneNumber() } - - let mutable billing_address: Shopping.Checkout.Address = - { Lines = - [ yield faker.Address.StreetName() + " " + faker.Address.BuildingNumber() - yield faker.Address.ZipCode() + " " + faker.Address.City() - yield faker.Address.County() ] - Country = faker.Address.CountryCode() } - - yield - checkout_stream, - Shopping.ShippingMethodSelected - { Cart = $"{cart_id}@{cart_version}" - Method = shipping_method - At = clock.GetCurrentInstant().ToDateTimeOffset() } - - yield - checkout_stream, - Shopping.ShippingCostCalculated - { Cart = $"{cart_id}@{cart_version}" - ForMethod = shipping_method - Cost = - match shipping_method with - | Shopping.Checkout.ShippingMethod.Express -> - faker.Commerce.Price(5.0m, 20.00m, 2, "USD") - | Shopping.Checkout.ShippingMethod.Overnight -> - faker.Commerce.Price(10.0m, 30.00m, 2, "USD") - | Shopping.Checkout.ShippingMethod.SameDay -> - faker.Commerce.Price(15.0m, 40.00m, 2, "USD") - | _ -> faker.Commerce.Price(0.0m, 10.00m, 2, "USD") - At = clock.GetCurrentInstant().ToDateTimeOffset() } - - clock.AdvanceTimeBetweenActions - faker - configuration.Shopping.TimeBetweenCheckoutActions.Minimum - configuration.Shopping.TimeBetweenCheckoutActions.Maximum - - if faker.Random.Bool() then - yield - checkout_stream, - Shopping.BillingInformationCollected - { Cart = $"{cart_id}@{cart_version}" - Recipient = billing_recipient - Address = billing_address - At = clock.GetCurrentInstant().ToDateTimeOffset() } - else - billing_recipient <- recipient - billing_address <- address - - yield - checkout_stream, - Shopping.BillingInformationCopiedFromShippingInformation - { Cart = $"{cart_id}@{cart_version}" - Recipient = recipient - Address = address - At = clock.GetCurrentInstant().ToDateTimeOffset() } - - clock.AdvanceTimeBetweenActions - faker - configuration.Shopping.TimeBetweenCheckoutActions.Minimum - configuration.Shopping.TimeBetweenCheckoutActions.Maximum - - let payment_method = faker.PickRandom() - - yield - checkout_stream, - Shopping.PaymentMethodSelected - { Cart = $"{cart_id}@{cart_version}" - Method = payment_method - At = clock.GetCurrentInstant().ToDateTimeOffset() } - - clock.AdvanceTimeBetweenActions - faker - configuration.Shopping.TimeBetweenCheckoutActions.Minimum - configuration.Shopping.TimeBetweenCheckoutActions.Maximum - - yield - checkout_stream, - Shopping.CheckoutCompleted - { Cart = $"{cart_id}@{cart_version}" - OrderId = order_id - At = clock.GetCurrentInstant().ToDateTimeOffset() } - - yield - cart_stream, - Shopping.CartGotCheckedOut - { CartId = cart_id - OrderId = order_id - At = clock.GetCurrentInstant().ToDateTimeOffset() } - - yield - order_stream, - Shopping.OrderPlaced - { OrderId = order_id - CustomerId = customer_id - CheckoutOfCart = $"{cart_id}@{cart_version}" - LineItems = products_in_cart.ToArray() |> List.ofArray - Shipping = - { Recipient = - { Title = recipient.Title - FullName = recipient.FullName - EmailAddress = recipient.EmailAddress - PhoneNumber = recipient.PhoneNumber } - Address = - { Lines = address.Lines - Country = address.Country } - Instructions = "" - Method = - match shipping_method with - | Shopping.Checkout.ShippingMethod.Express -> - Shopping.OrderFulfillment.OrderShippingMethod.Express - | Shopping.Checkout.ShippingMethod.Overnight -> - Shopping.OrderFulfillment.OrderShippingMethod.Overnight - | Shopping.Checkout.ShippingMethod.SameDay -> - Shopping.OrderFulfillment.OrderShippingMethod.SameDay - | Shopping.Checkout.ShippingMethod.Standard -> - Shopping.OrderFulfillment.OrderShippingMethod.Standard - | _ -> failwith "Unknown shipping method" } - Billing = - { Recipient = - { Title = billing_recipient.Title - FullName = billing_recipient.FullName - EmailAddress = billing_recipient.EmailAddress - PhoneNumber = billing_recipient.PhoneNumber } - Address = - { Lines = billing_address.Lines - Country = billing_address.Country } - PaymentMethod = - match payment_method with - | Shopping.Checkout.PaymentMethod.CreditCard -> - Shopping.OrderFulfillment.OrderPaymentMethod.CreditCard - | Shopping.Checkout.PaymentMethod.DebitCard -> - Shopping.OrderFulfillment.OrderPaymentMethod.DebitCard - | Shopping.Checkout.PaymentMethod.WireTransfer -> - Shopping.OrderFulfillment.OrderPaymentMethod.WireTransfer - | _ -> failwith "Unknown payment method" } - At = clock.GetCurrentInstant().ToDateTimeOffset() } - else - clock.Advance configuration.Shopping.AbandonCartAfterTime - - yield - cart_stream, - Shopping.CartGotAbandoned - { CartId = cart_id - AfterBeingIdleFor = configuration.Shopping.AbandonCartAfterTime.ToTimeSpan() - At = clock.GetCurrentInstant().ToDateTimeOffset() } - } diff --git a/commerce/Kurrent.Extensions.Commerce/StreamName.fs b/commerce/Kurrent.Extensions.Commerce/StreamName.fs deleted file mode 100644 index 75b767d..0000000 --- a/commerce/Kurrent.Extensions.Commerce/StreamName.fs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Kurrent.Extensions.Commerce - -type StreamName = private StreamName of string - -module StreamName = - let ofString value = StreamName value - let prefix value (StreamName suffix) = $"{value}-{suffix}" - let suffix value (StreamName prefix) = $"{prefix}-{value}" - let toString (StreamName name) = name diff --git a/commerce/Kurrent.Extensions.Commerce/makefile b/commerce/Kurrent.Extensions.Commerce/makefile deleted file mode 100644 index 6af9bf3..0000000 --- a/commerce/Kurrent.Extensions.Commerce/makefile +++ /dev/null @@ -1,7 +0,0 @@ -.PHONY: publish-edb-commerce -publish-edb-commerce: - dotnet publish -o ../.artifacts/linux-x64 -c release -p:PublishSingleFile=true --self-contained true -r linux-x64 -p:IncludeNativeLibrariesForSelfExtract=true - dotnet publish -o ../.artifacts/win-x64 -c release -p:PublishSingleFile=true --self-contained true -r win-x64 -p:IncludeNativeLibrariesForSelfExtract=true - dotnet publish -o ../.artifacts/linux-arm64 -c release -p:PublishSingleFile=true --self-contained true -r linux-arm64 -p:IncludeNativeLibrariesForSelfExtract=true - dotnet publish -o ../.artifacts/osx-x64 -c release -p:PublishSingleFile=true --self-contained true -r osx-x64 -p:IncludeNativeLibrariesForSelfExtract=true - dotnet publish -o ../.artifacts/osx-arm64 -c release -p:PublishSingleFile=true --self-contained true -r osx-arm64 -p:IncludeNativeLibrariesForSelfExtract=true diff --git a/commerce/global.json b/commerce/global.json deleted file mode 100644 index 31f23b6..0000000 --- a/commerce/global.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "sdk": { - "version": "9.0.201" - } -} \ No newline at end of file diff --git a/mix-and-match-database/.gitattributes b/mix-and-match-database/.gitattributes index 994a3b5..01b887c 100644 --- a/mix-and-match-database/.gitattributes +++ b/mix-and-match-database/.gitattributes @@ -1,9 +1,3 @@ *.sh text eol=lf -tools/Kurrent.Extensions.Commerce/osx-arm64/edb-commerce filter=lfs diff=lfs merge=lfs -text -tools/Kurrent.Extensions.Commerce/osx-arm64/libduckdb.dylib filter=lfs diff=lfs merge=lfs -text -tools/Kurrent.Extensions.Commerce/osx-x64/edb-commerce filter=lfs diff=lfs merge=lfs -text -tools/Kurrent.Extensions.Commerce/osx-x64/libduckdb.dylib filter=lfs diff=lfs merge=lfs -text -tools/Kurrent.Extensions.Commerce/linux-x64/edb-commerce filter=lfs diff=lfs merge=lfs -text -tools/Kurrent.Extensions.Commerce/win-x64/edb-commerce.exe filter=lfs diff=lfs merge=lfs -text -tools/Kurrent.Extensions.Commerce/linux-arm64/edb-commerce filter=lfs diff=lfs merge=lfs -text +tools/Kurrent.Extensions.Commerce/linux-x64/edb-commerce filter=lfs diff=lfs merge=lfs -text \ No newline at end of file diff --git a/mix-and-match-database/MongoProjection/MongoProjection.csproj b/mix-and-match-database/MongoProjection/MongoProjection.csproj deleted file mode 100644 index 384814c..0000000 --- a/mix-and-match-database/MongoProjection/MongoProjection.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - Exe - net9.0 - enable - enable - - - - - - - - \ No newline at end of file diff --git a/mix-and-match-database/MongoProjection/Program.cs b/mix-and-match-database/MongoProjection/Program.cs deleted file mode 100644 index d6b9e6d..0000000 --- a/mix-and-match-database/MongoProjection/Program.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System.Text; -using System.Text.Json; -using EventStore.Client; -using MongoDB.Driver; -using MongoDB.Bson; -using StreamPosition = EventStore.Client.StreamPosition; - -Console.WriteLine($"{AppDomain.CurrentDomain.FriendlyName} started"); - -// Connect to MongoDB -var mongoHost = Environment.GetEnvironmentVariable("MONGO_HOST") ?? "localhost"; -var mongoCollection = new MongoClient($"mongodb://{mongoHost}:27017").GetDatabase("mix-and-match-database").GetCollection("total-payment"); - -// Connect to KurrentDB -var esdbHost = Environment.GetEnvironmentVariable("ESDB_HOST") ?? "localhost"; -var esdb = new EventStoreClient(EventStoreClientSettings.Create($"esdb://admin:changeit@{esdbHost}:2113?tls=false")); - -var checkpointValue = mongoCollection // Get the checkpoint value from MongoDB.. - .Find(Builders.Filter.Eq("_id", "total")) // from the total document's.. - .FirstOrDefault()?["checkpoint"]?.AsInt64; // checkpoint field - -var streamPosition = checkpointValue.HasValue // Check if the checkpoint exists.. - ? FromStream.After(StreamPosition.FromInt64(checkpointValue.Value)) // If so, subscribe from stream after checkpoint.. - : FromStream.Start; // Otherwise, subscribe from the start of the stream - -await using var subscription = - esdb.SubscribeToStream( // Subscribe events.. - "$ce-payment", // from this stream.. - streamPosition, // from this position.. - true); // with linked events automatically resolved (required for system projections) - -Console.WriteLine($"Subscribing events from stream after {streamPosition}"); - -await foreach (var message in subscription.Messages) // Iterate through the messages in the subscription -{ - if (message is not StreamMessage.Event(var e)) continue; // Skip if message is not an event - - var @event = JsonSerializer.Deserialize( // Deserialize the event - Encoding.UTF8.GetString(e.Event.Data.Span)); - - if (@event == null) continue; // Skip if deserialization failed - - var updateCommand = Builders.Update // Create command to update both the read model and checkpoint in a single operation - .Inc("total", (double)(@event.amount ?? 0)) - .Set("checkpoint", e.OriginalEventNumber.ToInt64()); - - mongoCollection.UpdateOne( // Update the mongo.. - new BsonDocument("_id", "total"), // for the total document.. - updateCommand, // with the update command.. - new UpdateOptions { IsUpsert = true }); // and create it if it doesn't exist - - Console.WriteLine($"Updated MongoDB document 'total'. " + - $"Incremented total by {@event.amount}, " + - $"checkpoint set to {e.OriginalEventNumber.ToInt64()}"); -} - -public record PaymentEvent -{ - public decimal? amount { get; set; } - public DateTime timeStamp { get; set; } -} \ No newline at end of file diff --git a/mix-and-match-database/PostgresProjection/Program.cs b/mix-and-match-database/PostgresProjection/Program.cs index 16070e0..37b798f 100644 --- a/mix-and-match-database/PostgresProjection/Program.cs +++ b/mix-and-match-database/PostgresProjection/Program.cs @@ -45,12 +45,12 @@ // Connect to KurrentDB // // -------------------- // -var esdbHost = Environment.GetEnvironmentVariable("ESDB_HOST") // Get the KurrentDB host from environment variable +var kurrentDbHost = Environment.GetEnvironmentVariable("KURRENTDB_HOST") // Get the KurrentDB host from environment variable ?? "localhost"; // Default to localhost if not set -var esdb = new EventStoreClient( // Create a connection to KurrentDB +var kurrentdb = new EventStoreClient( // Create a connection to KurrentDB EventStoreClientSettings.Create( - $"esdb://admin:changeit@{esdbHost}:2113?tls=false")); + $"esdb://admin:changeit@{kurrentDbHost}:2113?tls=false")); // --------------------------------------------------- // // Retrieve the last checkpoint position from Postgres // @@ -69,7 +69,7 @@ // Subscribe to KurrentDB from checkpoint onwards // // ---------------------------------------------- // -await using var subscription = esdb.SubscribeToStream( // Subscribe events.. +await using var subscription = kurrentdb.SubscribeToStream( // Subscribe events.. "$ce-cart", // from the cart category system projection.. streamPosition, // from this position.. true); // with linked events automatically resolved (required for system projections) diff --git a/mix-and-match-database/RedisProjection/Program.cs b/mix-and-match-database/RedisProjection/Program.cs index 455b369..7233b1d 100644 --- a/mix-and-match-database/RedisProjection/Program.cs +++ b/mix-and-match-database/RedisProjection/Program.cs @@ -19,12 +19,12 @@ // Connect to KurrentDB // // -------------------- // -var esdbHost = Environment.GetEnvironmentVariable("ESDB_HOST") // Get the KurrentDB host from environment variable +var kurrentdbHost = Environment.GetEnvironmentVariable("KURRENTDB_HOST") // Get the KurrentDB host from environment variable ?? "localhost"; // Default to localhost if not set -var esdb = new EventStoreClient( // Create a connection to KurrentDB +var kurrentdb = new EventStoreClient( // Create a connection to KurrentDB EventStoreClientSettings.Create( - $"esdb://admin:changeit@{esdbHost}:2113?tls=false")); + $"esdb://admin:changeit@{kurrentdbHost}:2113?tls=false")); // ------------------------------------------------ // // Retrieve the last checkpoint position from Redis // @@ -39,7 +39,7 @@ // Subscribe to KurrentDB from checkpoint onwards // // ---------------------------------------------- // -await using var subscription = esdb.SubscribeToStream( // Subscribe events.. +await using var subscription = kurrentdb.SubscribeToStream( // Subscribe events.. "$ce-cart", // from the cart category system projection.. streamPosition, // from this position.. true); // with linked events automatically resolved (required for system projections) diff --git a/mix-and-match-database/data/datagen.live.config b/mix-and-match-database/data/datagen.live.config index aa34187..29fc70d 100644 --- a/mix-and-match-database/data/datagen.live.config +++ b/mix-and-match-database/data/datagen.live.config @@ -9,8 +9,8 @@ "maximum": 1000 }, "concurrentCartCount": { - "minimum": 100, - "maximum": 500 + "minimum": 300, + "maximum": 300 }, "cartActionCount": { "minimum": 1, diff --git a/mix-and-match-database/docker-compose.app.yml b/mix-and-match-database/docker-compose.app.yml deleted file mode 100644 index ea73fcc..0000000 --- a/mix-and-match-database/docker-compose.app.yml +++ /dev/null @@ -1,46 +0,0 @@ -services: - demoweb: - image: mcr.microsoft.com/dotnet/sdk:9.0 - container_name: demoweb - working_dir: /app - volumes: - - ./DemoWeb:/app # Mount the DemoWeb project directory - command: ["dotnet", "run"] - ports: - - "5108:5108" - environment: - - REDIS_HOST=redis - - POSTGRES_HOST=postgres - - postgresprojection: - image: mcr.microsoft.com/dotnet/sdk:9.0 - container_name: postgresprojection - working_dir: /app - volumes: - - ./:/app - command: ["dotnet", "run", "--project", "./PostgresProjection"] - environment: - - POSTGRES_HOST=postgres - - ESDB_HOST=eventstore - - redisprojection: - image: mcr.microsoft.com/dotnet/sdk:9.0 - container_name: redisprojection - working_dir: /app - volumes: - - ./:/app - command: ["dotnet", "run", "--project", "./RedisProjection"] - environment: - - REDIS_HOST=redis - - ESDB_HOST=eventstore - - mongoprojection: - image: mcr.microsoft.com/dotnet/sdk:9.0 - container_name: mongoprojection - working_dir: /app - volumes: - - ./:/app - command: ["dotnet", "run", "--project", "./MongoProjection"] - environment: - - MONGO_HOST=mongo - - ESDB_HOST=eventstore \ No newline at end of file diff --git a/mix-and-match-database/docker-compose.yml b/mix-and-match-database/docker-compose.yml index a0cc4c2..19a0b14 100644 --- a/mix-and-match-database/docker-compose.yml +++ b/mix-and-match-database/docker-compose.yml @@ -1,10 +1,5 @@ services: - mongo: - image: mongo:7.0 - container_name: mongo - ports: - - "27017:27017" - + postgres: image: postgres:16 container_name: postgres @@ -12,21 +7,62 @@ services: - "5432:5432" environment: - POSTGRES_HOST_AUTH_METHOD=trust + profiles: ["db"] redis: image: redis:7.2 container_name: redis ports: - "6379:6379" + profiles: ["db"] - eventstore: - image: eventstore/eventstore:24.10 - container_name: eventstore + kurrentdb: + image: kurrentplatform/kurrentdb:25.0 + container_name: kurrentdb ports: - "2113:2113" - "1113:1113" environment: - - EVENTSTORE_RUN_PROJECTIONS=All - - EVENTSTORE_START_STANDARD_PROJECTIONS=true - - EVENTSTORE_INSECURE=true - - EVENTSTORE_ENABLE_ATOM_PUB_OVER_HTTP=true \ No newline at end of file + - KURRENTDB_RUN_PROJECTIONS=All + - KURRENTDB_START_STANDARD_PROJECTIONS=true + - KURRENTDB_INSECURE=true + - KURRENTDB_ENABLE_ATOM_PUB_OVER_HTTP=true + profiles: ["db"] + + demoweb: + image: mcr.microsoft.com/dotnet/sdk:9.0 + container_name: demoweb + working_dir: /app + volumes: + - ./DemoWeb:/app # Mount the DemoWeb project directory + command: ["dotnet", "run"] + ports: + - "5108:5108" + environment: + - REDIS_HOST=redis + - POSTGRES_HOST=postgres + profiles: ["app"] + + postgresprojection: + image: mcr.microsoft.com/dotnet/sdk:9.0 + container_name: postgresprojection + working_dir: /app + volumes: + - ./:/app + command: ["dotnet", "run", "--project", "./PostgresProjection"] + environment: + - POSTGRES_HOST=postgres + - KURRENTDB_HOST=kurrentdb + profiles: ["app"] + + redisprojection: + image: mcr.microsoft.com/dotnet/sdk:9.0 + container_name: redisprojection + working_dir: /app + volumes: + - ./:/app + command: ["dotnet", "run", "--project", "./RedisProjection"] + environment: + - REDIS_HOST=redis + - KURRENTDB_HOST=kurrentdb + profiles: ["app"] \ No newline at end of file diff --git a/mix-and-match-database/mix-and-match-database.sln b/mix-and-match-database/mix-and-match-database.sln index 9e30295..b690b3d 100644 --- a/mix-and-match-database/mix-and-match-database.sln +++ b/mix-and-match-database/mix-and-match-database.sln @@ -17,8 +17,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".devcontainer", ".devcontai .devcontainer\Dockerfile = .devcontainer\Dockerfile EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MongoProjection", "MongoProjection\MongoProjection.csproj", "{447E20F5-76DA-492D-883F-189EA7FAB8DE}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PostgresProjection", "PostgresProjection\PostgresProjection.csproj", "{CB037517-E7CA-4FE1-96B1-EA286A739DDC}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DemoWeb", "DemoWeb\DemoWeb.csproj", "{748E0FE3-4DD1-4B6F-9588-8F06D60229E6}" diff --git a/mix-and-match-database/scripts/1-init-data.sh b/mix-and-match-database/scripts/1-init-data.sh index d9470d0..6630015 100644 --- a/mix-and-match-database/scripts/1-init-data.sh +++ b/mix-and-match-database/scripts/1-init-data.sh @@ -42,16 +42,16 @@ mv "$data_dir/data.json" "$data_init_path" # Seed the data using the edb-commerce tool with the updated initialization JSON "$edbcommerce" seed-data-set "$data_init_path" -ESDB_URL=http://localhost:2113 # Set default URL to localhost (for KurrentDB started locally, not in Codespaces) +KURRENTDB_URL=http://localhost:2113 # Set default URL to localhost (for KurrentDB started locally, not in Codespaces) if [ "$CODESPACES" == "true" ]; then # If this environment is Codespaces - ESDB_URL=https://"$CODESPACE_NAME"-2113.$GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN # Build the URL to forwarded github codespaces domain + KURRENTDB_URL=https://"$CODESPACE_NAME"-2113.$GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN # Build the URL to forwarded github codespaces domain fi echo "" echo "" echo -e "🚀 \e[32mKurrentDB Server has started!!\e[0m 🚀" echo "" -echo -e "URL to KurrentDB Admin UI 👉 \e[0m \e[34m$ESDB_URL\e[0m" # Print URL to KurrentDB Admin UI +echo -e "URL to KurrentDB Admin UI 👉 \e[0m \e[34m$KURRENTDB_URL/web/index.html\e[0m" # Print URL to KurrentDB Admin UI echo "" echo "" echo "Appended sample data to KurrentDB" diff --git a/mix-and-match-database/scripts/4-start-live-data-gen.sh b/mix-and-match-database/scripts/4-start-live-data-gen.sh index 032b0d3..63e99d2 100644 --- a/mix-and-match-database/scripts/4-start-live-data-gen.sh +++ b/mix-and-match-database/scripts/4-start-live-data-gen.sh @@ -15,14 +15,14 @@ if [ ! -d "$root_path/data" ]; then fi - ESDB_URL=http://localhost:2113 # Set default URL to localhost (for KurrentDB started locally, not in Codespaces) + KURRENTDB_URL=http://localhost:2113 # Set default URL to localhost (for KurrentDB started locally, not in Codespaces) if [ "$CODESPACES" == "true" ]; then # If this environment is Codespaces - ESDB_URL=https://"$CODESPACE_NAME"-2113.$GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN # Build the URL to forwarded github codespaces domain + KURRENTDB_URL=https://"$CODESPACE_NAME"-2113.$GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN # Build the URL to forwarded github codespaces domain fi echo "" echo "" -echo -e "URL to KurrentDB Admin UI 👉 \e[0m \e[34m$ESDB_URL\e[0m" # Print URL to KurrentDB Admin UI +echo -e "URL to KurrentDB Admin UI 👉 \e[0m \e[34m$KURRENTDB_URL/web/index.html\e[0m" # Print URL to KurrentDB Admin UI echo "" DEMOWEB_URL=http://localhost:5108 # Set default URL to localhost diff --git a/mix-and-match-database/scripts/start-app.sh b/mix-and-match-database/scripts/start-app.sh index 4c90a2e..aaa5561 100644 --- a/mix-and-match-database/scripts/start-app.sh +++ b/mix-and-match-database/scripts/start-app.sh @@ -14,9 +14,7 @@ if [ ! -d "$root_path/data" ]; then exit 1 fi -docker_compose_file="$root_path/docker-compose.app.yml" - -docker compose -f "$docker_compose_file" up -d +docker compose --profile app -f "$root_path/docker-compose.yml" up -d max_attempts=60 attempt=0 @@ -36,18 +34,19 @@ echo "DemoWeb is running." max_attempts=60 attempt=0 while true; do - logs=$(docker compose -f "$docker_compose_file" logs 2>&1) - if echo "$logs" | grep -q "MongoProjection started" && \ - echo "$logs" | grep -q "RedisProjection started" && \ + logs=$(docker compose --profile app -f "$root_path/docker-compose.yml" logs 2>&1) + if echo "$logs" | grep -q "RedisProjection started" && \ echo "$logs" | grep -q "PostgresProjection started"; then echo "All projection apps are running." break fi attempt=$((attempt+1)) + if [ $attempt -ge $max_attempts ]; then echo "Required projections did not start after $max_attempts attempts. Exiting." exit 1 fi echo "Waiting projection apps to start... (attempt $attempt)" + sleep 2 done \ No newline at end of file diff --git a/mix-and-match-database/scripts/start-db.sh b/mix-and-match-database/scripts/start-db.sh index 09e2764..cbbb43d 100644 --- a/mix-and-match-database/scripts/start-db.sh +++ b/mix-and-match-database/scripts/start-db.sh @@ -14,7 +14,7 @@ if [ ! -d "$root_path/data" ]; then exit 1 fi -docker compose -f "$root_path/docker-compose.yml" up -d +docker compose --profile db -f "$root_path/docker-compose.yml" up -d max_attempts=60 attempt=0 diff --git a/mix-and-match-database/tools/Kurrent.Extensions.Commerce/linux-arm64/edb-commerce b/mix-and-match-database/tools/Kurrent.Extensions.Commerce/linux-arm64/edb-commerce deleted file mode 100644 index 9c6eb65..0000000 --- a/mix-and-match-database/tools/Kurrent.Extensions.Commerce/linux-arm64/edb-commerce +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:934b8f4e4749c6177f4917bd7cfee3854684f43a6fe24502d22851d4f8d8b1c9 -size 144896471 diff --git a/mix-and-match-database/tools/Kurrent.Extensions.Commerce/osx-arm64/edb-commerce b/mix-and-match-database/tools/Kurrent.Extensions.Commerce/osx-arm64/edb-commerce deleted file mode 100644 index 489cc25..0000000 --- a/mix-and-match-database/tools/Kurrent.Extensions.Commerce/osx-arm64/edb-commerce +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:74d75625af5e5bb9718049cbaf3ad044b8355c459ad7e460188a54217f54e098 -size 89480394 diff --git a/mix-and-match-database/tools/Kurrent.Extensions.Commerce/osx-arm64/libduckdb.dylib b/mix-and-match-database/tools/Kurrent.Extensions.Commerce/osx-arm64/libduckdb.dylib deleted file mode 100644 index 50ad999..0000000 --- a/mix-and-match-database/tools/Kurrent.Extensions.Commerce/osx-arm64/libduckdb.dylib +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:007f820901b91cbe0abf54c41462cb7c4f49bbe156c6aee49ee790c16f72a58e -size 102351312 diff --git a/mix-and-match-database/tools/Kurrent.Extensions.Commerce/osx-x64/edb-commerce b/mix-and-match-database/tools/Kurrent.Extensions.Commerce/osx-x64/edb-commerce deleted file mode 100644 index 1de6b6a..0000000 --- a/mix-and-match-database/tools/Kurrent.Extensions.Commerce/osx-x64/edb-commerce +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ee65da752085d999a1df6d5aa3f91fb5765d5397cbce6400f20c536bde3a036b -size 82571004 diff --git a/mix-and-match-database/tools/Kurrent.Extensions.Commerce/osx-x64/libduckdb.dylib b/mix-and-match-database/tools/Kurrent.Extensions.Commerce/osx-x64/libduckdb.dylib deleted file mode 100644 index 50ad999..0000000 --- a/mix-and-match-database/tools/Kurrent.Extensions.Commerce/osx-x64/libduckdb.dylib +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:007f820901b91cbe0abf54c41462cb7c4f49bbe156c6aee49ee790c16f72a58e -size 102351312 diff --git a/mix-and-match-database/tools/Kurrent.Extensions.Commerce/win-x64/edb-commerce.exe b/mix-and-match-database/tools/Kurrent.Extensions.Commerce/win-x64/edb-commerce.exe deleted file mode 100644 index ddf1aac..0000000 --- a/mix-and-match-database/tools/Kurrent.Extensions.Commerce/win-x64/edb-commerce.exe +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:187e5669580c4307f559aba1ff4c8f89757abe11e245c4a3e9c1890ca6b696dd -size 112591685