Supabase Starter Kit For Event-Driven Applications
Foundation for building compositional, safe and ergonomic applications
#EventSourcing #EdgeFunctions
Supabase SQL migration scripts and TypeScript edge functions included.
$ supabase start
Technology: Next.js 14, React, TypeScript, TailwindCSS, Shadcn/Ui
$ npm run dev
Focus on the business logic - the Decider, and run it as the Edge function. Combine many deciders to compose and evolve command handling capabilities.
Deno.serve(async (cmd: Request) => await edgeCommandHandler(
cmd,
commandAndMetadataSchema,
restaurantDecider.combine(orderDecider),
),
);
Focus on the business logic - the View, and run it as the Edge function. Combine many views to compose and evolve event handling capabilities.
Deno.serve(async (evt: Request) => await edgeEventHandler(
evt,
eventAndMetadataSchema,
restaurantView.combine(orderView),
new RestaurantViewStateRepository(
authenticatedClient(evt),
)));
Specify the event types that comand handler can publish.
Stream events to concurent consumers, and track the progress.
Achieve complete automation of information flow and business processes
#Discover #Design #Develop #Deploy
Event Modeling is an effective technique to discover a domain. It is a method of describing systems using an example of how information has changed within them over time. Specifically this omits transient details and looks at what is durably stored and what the user sees at any particular point in time. These are the events on the timeline that form the description of the system.
We can show, by example, what a system is supposed to do from start to finish, on a time line and with no branching.
Specification By Example is a collaborative approach to software analysis and testing. It is the fastest way to align people from different roles on what exactly we need to build and how to test it.
The requirements are presented as scenarios. A scenario is an example of the system’s behavior from the users’ perspective, and they are specified using the Given-When-Then structure to create a testable specification.
The specification is written in the form of executable tests that are automated and can be run continuously. Tests cover the full range of scenarios, from happy paths to errors.
The f { model } library is implementing the event model in a general way. It promotes clear separation between data and behaviour.
The flow is parametrized with Command, Event, and State parameters. The responsibility of the business is to specialize in their case by specifying concrete Commands, Events, and State. For example, Commands=CreateOrder, AddItemToOrder; Events=OrderCreated, ItemAdded, State=Order(with list of Items).
You can run three types of functions on the platform:
1. Command-handlers (deciders) - responsible for handling commands and producing new events/decisions.
2. Event-handlers responsible for:
a) handling events and evolving the materiliazed view(s) state based on these events
b) automating integration with other systems, like payment providers.
3. Query-handlers (views) - responsible for querying the materilized view(s) state.
Moreover, it`s worth noting that these functions can be executed in a conventional manner, either on the server or directly within the database. They are not restricted solely to edge processing.
A pure command handling algorithm, responsible for making decisions/events based on the commands and the current state. It does not produce any side effects, such as I/O, logging, etc. It is written in the TypeScript programming language and utilizes type narrowing to make sure that the command is handled exhaustively.
The infrastructure is already implemented for you and it is supporting the Edge scenario, in where we use Supabase API to store and fetch events, so you can focus on the business logic and the core domain model!
export const restaurantDecider: Decider<
RestaurantCommand,
Restaurant | null,
RestaurantEvent
> = new Decider<RestaurantCommand, Restaurant | null, RestaurantEvent>(
(command, currentState) => {
switch (command.kind) {
case "CreateRestaurantCommand":
return (currentState === null ||
currentState.restaurantId === undefined)
? [
{
version: 1,
decider: "Restaurant",
kind: "RestaurantCreatedEvent",
id: command.id,
name: command.name,
menu: command.menu,
final: false,
},
]
: [
{
version: 1,
decider: "Restaurant",
kind: "RestaurantNotCreatedEvent",
id: command.id,
name: command.name,
menu: command.menu,
reason: "Restaurant already exist!",
final: false,
},
];
case "ChangeRestaurantMenuCommand":
return (currentState !== null &&
currentState.restaurantId === command.id)
? [
{
version: 1,
decider: "Restaurant",
kind: "RestaurantMenuChangedEvent",
id: currentState.restaurantId,
menu: command.menu,
final: false,
},
]
: [
{
version: 1,
decider: "Restaurant",
kind: "RestaurantMenuNotChangedEvent",
id: command.id,
menu: command.menu,
reason: "Restaurant does not exist!",
final: false,
},
];
case "PlaceOrderCommand":
return (currentState !== null &&
currentState.restaurantId === command.id)
? [
{
version: 1,
decider: "Restaurant",
kind: "RestaurantOrderPlacedEvent",
id: command.id,
orderId: command.orderId,
menuItems: command.menuItems,
final: false,
},
]
: [
{
version: 1,
decider: "Restaurant",
kind: "RestaurantOrderNotPlacedEvent",
id: command.id,
orderId: command.orderId,
menuItems: command.menuItems,
reason: "Restaurant does not exist!",
final: false,
},
];
default:
// Exhaustive matching of the command type
const _: never = command;
return [];
}
},
(currentState, event) => {
switch (event.kind) {
case "RestaurantCreatedEvent":
return { restaurantId: event.id, name: event.name, menu: event.menu };
case "RestaurantNotCreatedEvent":
return currentState;
case "RestaurantMenuChangedEvent":
return currentState !== null
? {
restaurantId: currentState.restaurantId,
name: currentState.name,
menu: event.menu,
}
: currentState;
case "RestaurantMenuNotChangedEvent":
return currentState;
case "RestaurantOrderPlacedEvent":
return currentState;
case "RestaurantOrderNotPlacedEvent":
return currentState;
default:
// Exhaustive matching of the event type
const _: never = event;
return currentState;
}
},
null,
);
export const restaurantView: View<RestaurantView | null, RestaurantEvent> =
new View<RestaurantView | null, RestaurantEvent>(
(currentState, event) => {
switch (event.kind) {
case "RestaurantCreatedEvent":
return { restaurantId: event.id, name: event.name, menu: event.menu };
case "RestaurantNotCreatedEvent":
return currentState;
case "RestaurantMenuChangedEvent":
return currentState !== null
? {
restaurantId: currentState.restaurantId,
name: currentState.name,
menu: event.menu,
}
: currentState;
case "RestaurantMenuNotChangedEvent":
return currentState;
case "RestaurantOrderPlacedEvent":
return currentState;
case "RestaurantOrderNotPlacedEvent":
return currentState;
// deno-lint-ignore no-case-declarations
default:
// Exhaustive matching of the event type
const _: never = event;
return currentState;
}
},
null,
);
A pure event handling algorithm, responsible for evolving the state of the view/projection. It does not produce any side effects, such as I/O, logging, etc. It is written in the TypeScript programming language and utilizes type narrowing to make sure that the event is handled exhaustively.
As your event handler is responsible for evolving the state of the materilized view, the query handler is responsible for querying the materilized view state. Supabase will create the API on top of the Postgres database, so you can query the materilized view state. You can choose to create an edge function to handle the query or use the Supabase API directly.
Event Handlers track the progress of the process by storing the position/offset of the last event that was processed successfully. This is what makes them Process Managers, and they can be used to implement Sagas, Orchestrators, and other process management/integration patterns.