Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 57 additions & 18 deletions ruby_event_store-browser/devserver/config.ru
Original file line number Diff line number Diff line change
Expand Up @@ -17,50 +17,89 @@ sample_data = {
some_float_infinity_attribute: 1.0 / 0,
}

sample_event_type =
lambda do
namespaces = %w[IdentityAndAccess Subscriptions Payments Accounting Banking Reporting]
module IdentityAndAccess
SomethingHappened = Class.new(RubyEventStore::Event)
SomeoneDidThing = Class.new(RubyEventStore::Event)
ThingConfirmed = Class.new(RubyEventStore::Event)
ThingRejected = Class.new(RubyEventStore::Event)
end

module Subscriptions
SomethingHappened = Class.new(RubyEventStore::Event)
SomeoneDidThing = Class.new(RubyEventStore::Event)
ThingConfirmed = Class.new(RubyEventStore::Event)
ThingRejected = Class.new(RubyEventStore::Event)
end

module Payments
SomethingHappened = Class.new(RubyEventStore::Event)
SomeoneDidThing = Class.new(RubyEventStore::Event)
ThingConfirmed = Class.new(RubyEventStore::Event)
ThingRejected = Class.new(RubyEventStore::Event)
end

module Accounting
SomethingHappened = Class.new(RubyEventStore::Event)
SomeoneDidThing = Class.new(RubyEventStore::Event)
ThingConfirmed = Class.new(RubyEventStore::Event)
ThingRejected = Class.new(RubyEventStore::Event)
end

module Banking
SomethingHappened = Class.new(RubyEventStore::Event)
SomeoneDidThing = Class.new(RubyEventStore::Event)
ThingConfirmed = Class.new(RubyEventStore::Event)
ThingRejected = Class.new(RubyEventStore::Event)
end

# Reporting namespace intentionally not defined - these will use event_type metadata for testing
# the events which don't have the event classes available in the namespace

sample_event_class =
lambda do
namespaces = %w[IdentityAndAccess Subscriptions Payments Accounting Banking]
events = %w[SomethingHappened SomeoneDidThing ThingConfirmed ThingRejected]

[namespaces.sample, events.sample].join("::")
namespace = namespaces.sample
event = events.sample
Object.const_get("#{namespace}::#{event}")
end

sample_event_type_without_class =
lambda do
events = %w[SomethingHappened SomeoneDidThing ThingConfirmed ThingRejected]
"Reporting::#{events.sample}"
end

event_store.publish(
90.times.map { RubyEventStore::Event.new(data: sample_data, metadata: { event_type: sample_event_type.call }) },
80
.times
.map { sample_event_class.call.new(data: sample_data) } +
10.times.map { RubyEventStore::Event.new(data: sample_data, metadata: { event_type: sample_event_type_without_class.call }) },
stream_name: "DummyStream$78",
)

other_event =
RubyEventStore::Event.new(
data: sample_data,
metadata: {
event_type: sample_event_type.call,
correlation_id: "469904c5-46ee-43a3-857f-16a455cfe337",
},
)
other_event = sample_event_class.call.new(data: sample_data, metadata: { correlation_id: "469904c5-46ee-43a3-857f-16a455cfe337" })

event_store.publish(other_event, stream_name: "OtherStream$91")
21.times do
event_store.with_metadata(
correlation_id: other_event.metadata[:correlation_id] || other_event.event_id,
causation_id: other_event.event_id,
) do
event_store.publish(
RubyEventStore::Event.new(data: sample_data, metadata: { event_type: sample_event_type.call }),
stream_name: "DummyStream$79",
)
event_store.publish(sample_event_class.call.new(data: sample_data), stream_name: "DummyStream$79")
end
end

RELATED_STREAMS_QUERY = ->(stream_name) do
stream_name.start_with?("$by_type_#{sample_event_type.call}") ? %w[all $by_type_#{sample_event_type.call}] : []
stream_name.start_with?("$by_type_") ? %w[all] : []
end

browser_app =
RubyEventStore::Browser::App.for(
event_store_locator: -> { event_store },
related_streams_query: RELATED_STREAMS_QUERY,
experimental_event_types_query: ->(es) { RubyEventStore::Browser::EventTypesQuerying::DefaultQuery.new(es) },
)
mount_point = "/"

Expand Down
32 changes: 31 additions & 1 deletion ruby_event_store-browser/elm/src/Api.elm
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module Api exposing (Event, PaginatedList, PaginationLink, PaginationLinks, RemoteResource(..), Stream, emptyPaginatedList, eventDecoder, eventsDecoder, getEvent, getEvents, getStream)
module Api exposing (Event, EventType, PaginatedList, PaginationLink, PaginationLinks, RemoteResource(..), Stream, emptyPaginatedList, eventDecoder, eventsDecoder, getEvent, getEventTypes, getEvents, getStream)

import Flags exposing (Flags)
import Http
Expand Down Expand Up @@ -65,6 +65,12 @@ type alias Stream =
}


type alias EventType =
{ eventType : String
, streamName : String
}


buildUrl : String -> String -> String
buildUrl baseUrl id =
baseUrl ++ "/" ++ Url.percentEncode id
Expand All @@ -85,6 +91,10 @@ streamUrl flags streamId =
buildUrl (Url.toString flags.apiUrl ++ "/streams") streamId


eventTypesUrl : Flags -> String
eventTypesUrl flags =
Url.toString flags.apiUrl ++ "/event_types"


getEvent : (Result Http.Error Event -> msg) -> Flags -> String -> Cmd msg
getEvent msgBuilder flags eventId =
Expand Down Expand Up @@ -142,6 +152,26 @@ streamDecoder_ =
|> optionalAt [ "attributes", "related_streams" ] (maybe (list string)) Nothing


getEventTypes : (Result Http.Error (List EventType) -> msg) -> Flags -> Cmd msg
getEventTypes msgBuilder flags =
Http.get
{ url = eventTypesUrl flags
, expect = Http.expectJson msgBuilder eventTypesDecoder
}


eventTypesDecoder : Decoder (List EventType)
eventTypesDecoder =
field "data" (list eventTypeDecoder_)


eventTypeDecoder_ : Decoder EventType
eventTypeDecoder_ =
succeed EventType
|> requiredAt [ "attributes", "event_type" ] string
|> requiredAt [ "attributes", "stream_name" ] string


getEvents : (Result Http.Error (PaginatedList Event) -> msg) -> Flags -> String -> Pagination.Specification -> Cmd msg
getEvents msgBuilder flags streamId paginationSpecification =
Http.get
Expand Down
24 changes: 24 additions & 0 deletions ruby_event_store-browser/elm/src/Main.elm
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import Html exposing (..)
import Layout
import LinkedTimezones exposing (mapLinkedTimeZone)
import Page.ShowEvent
import Page.ShowEventTypes
import Page.ShowStream
import Route
import Task
Expand Down Expand Up @@ -47,13 +48,15 @@ type Msg
| ClickedLink Browser.UrlRequest
| GotLayoutMsg Layout.Msg
| GotShowEventMsg Page.ShowEvent.Msg
| GotShowEventTypesMsg Page.ShowEventTypes.Msg
| GotShowStreamMsg Page.ShowStream.Msg
| ReceiveTimeZone (Result String Time.ZoneName)


type Page
= NotFound
| ShowEvent Page.ShowEvent.Model
| ShowEventTypes Page.ShowEventTypes.Model
| ShowStream Page.ShowStream.Model


Expand Down Expand Up @@ -128,6 +131,15 @@ update msg model =
, Cmd.map GotShowEventMsg subCmd
)

( GotShowEventTypesMsg showEventTypesUIMsg, ShowEventTypes showEventTypesModel ) ->
let
( subModel, subCmd ) =
Page.ShowEventTypes.update showEventTypesUIMsg showEventTypesModel
in
( { model | page = ShowEventTypes subModel }
, Cmd.map GotShowEventTypesMsg subCmd
)

( GotLayoutMsg layoutMsg, _ ) ->
case model.flags of
Nothing ->
Expand Down Expand Up @@ -212,6 +224,11 @@ navigate model location =
Nothing ->
( { model | page = NotFound }, Cmd.none )

Just Route.ShowEventTypes ->
( { model | page = ShowEventTypes (Page.ShowEventTypes.initModel flags) }
, Cmd.map GotShowEventTypesMsg (Page.ShowEventTypes.initCmd flags)
)

Nothing ->
( { model | page = NotFound }, Cmd.none )

Expand Down Expand Up @@ -264,5 +281,12 @@ viewPage page selectedTime =
in
( Just title, Html.map GotShowEventMsg content )

ShowEventTypes pageModel ->
let
( title, content ) =
Page.ShowEventTypes.view pageModel selectedTime
in
( Just title, Html.map GotShowEventTypesMsg content )

NotFound ->
( Nothing, Layout.viewNotFound )
136 changes: 136 additions & 0 deletions ruby_event_store-browser/elm/src/Page/ShowEventTypes.elm
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
module Page.ShowEventTypes exposing (Model, Msg(..), initCmd, initModel, update, view)

import Api
import BrowserTime
import Flags exposing (Flags)
import Html exposing (..)
import Html.Attributes exposing (class, href, title)
import Http
import Route
import Url


-- MODEL


type alias Model =
{ eventTypes : List Api.EventType
, flags : Flags
, problems : List Problem
}


type Problem
= ServerError String
| FeatureNotEnabled


initModel : Flags -> Model
initModel flags =
{ eventTypes = []
, flags = flags
, problems = []
}


-- UPDATE


type Msg
= EventTypesFetched (Result Http.Error (List Api.EventType))


initCmd : Flags -> Cmd Msg
initCmd flags =
Api.getEventTypes EventTypesFetched flags


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
EventTypesFetched (Ok result) ->
( { model | eventTypes = result }, Cmd.none )

EventTypesFetched (Err error) ->
let
problem =
case error of
Http.BadStatus 422 ->
FeatureNotEnabled

_ ->
ServerError "Server error, please check backend logs for details"
in
( { model | problems = [ problem ] }, Cmd.none )


-- VIEW


view : Model -> BrowserTime.TimeZone -> ( String, Html Msg )
view { eventTypes, problems, flags } _ =
let
title =
"Event Types"

header =
"Event Types"
in
case problems of
[] ->
( title
, viewEventTypes flags.rootUrl header eventTypes
)

_ ->
( title
, div [ class "py-8" ]
[ div []
[ ul []
(List.map
(\problem ->
case problem of
ServerError error ->
li [] [ text error ]

FeatureNotEnabled ->
li [ class "text-gray-700" ]
[ p [ class "font-semibold mb-2" ] [ text "Event Types feature is not enabled" ]
, p [ class "text-sm" ]
[ text "This experimental feature must be explicitly enabled when configuring the Browser. "
, text "Please check the documentation for configuration details."
]
]
)
problems
)
]
]
)


viewEventTypes : Url.Url -> String -> List Api.EventType -> Html Msg
viewEventTypes rootUrl header eventTypes =
div [ class "py-8" ]
[ h1 [ class "font-semibold text-2xl mb-4" ] [ text header ]
, if List.isEmpty eventTypes then
p [ class "text-gray-500" ] [ text "No event types found" ]

else
ul [ class "space-y-2" ]
(List.map (viewEventType rootUrl) eventTypes)
]


viewEventType : Url.Url -> Api.EventType -> Html Msg
viewEventType rootUrl eventType =
li [ class "border-b border-gray-200 py-2" ]
[ span [ class "font-medium" ] [ text eventType.eventType ]
, span [ class "text-gray-500 text-sm mx-2" ] [ text "→" ]
, a
[ href (Route.streamUrl rootUrl eventType.streamName)
, class "text-blue-600 hover:text-blue-800 text-sm"
, title ("View " ++ eventType.eventType ++ " events")
]
[ text eventType.streamName ]
]
2 changes: 2 additions & 0 deletions ruby_event_store-browser/elm/src/Route.elm
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import Url.Parser.Query as Query
type Route
= BrowseEvents String Pagination.Specification
| ShowEvent String
| ShowEventTypes


decodeLocation : Url.Url -> Url.Url -> Maybe Route
Expand All @@ -25,6 +26,7 @@ routeParser =
[ Url.Parser.map (BrowseEvents "all" Pagination.empty) Url.Parser.top
, Url.Parser.map browseEvents (Url.Parser.s "streams" </> Url.Parser.string <?> Query.string "page[position]" <?> Query.string "page[direction]" <?> Query.string "page[count]")
, Url.Parser.map ShowEvent (Url.Parser.s "events" </> Url.Parser.string)
, Url.Parser.map ShowEventTypes (Url.Parser.s "types")
]


Expand Down
3 changes: 3 additions & 0 deletions ruby_event_store-browser/lib/ruby_event_store/browser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,6 @@ module Browser
require_relative "browser/urls"
require_relative "browser/gem_source"
require_relative "browser/router"
require_relative "browser/event_types_querying"
require_relative "browser/json_api_event_type"
require_relative "browser/get_event_types"
Loading
Loading