Skip to content

Commit

Permalink
first commit
Browse files Browse the repository at this point in the history
  • Loading branch information
idugalic committed Jun 25, 2024
0 parents commit 790d2e1
Show file tree
Hide file tree
Showing 29 changed files with 1,958 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[target.'cfg(target_os="macos")']
# Postgres symbols won't be available until runtime
rustflags = ["-Clink-arg=-Wl,-undefined,dynamic_lookup"]
42 changes: 42 additions & 0 deletions .github/workflows/lint-and-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: 🧪 Lint and Test

on:
push:
branches-ignore: [wip/**]

jobs:
test:
runs-on: ubuntu-latest
container: pgxn/pgxn-tools
strategy:
matrix:
pg: [11, 12, 13, 14, 15, 16]
name: 🐘 Postgres ${{ matrix.pg }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Start PostgreSQL ${{ matrix.pg }}
run: pg-start ${{ matrix.pg }}
- name: Setup Rust Cache
uses: Swatinem/rust-cache@v2
- name: Test on PostgreSQL ${{ matrix.pg }}
run: pgrx-build-test

lint:
name: ✅ Lint and Cover
runs-on: ubuntu-latest
container: pgxn/pgxn-tools
env: { PGVERSION: 16 }
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Start PostgreSQL ${{ env.PGVERSION }}
run: pg-start ${{ env.PGVERSION }} libxml2-utils
- name: Setup Rust Cache
uses: Swatinem/rust-cache@v2
- name: Install pgrx
run: make install-pgrx
- name: Initialize pgrx
run: make pgrx-init
- name: Format and Lint
run: make lint
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.DS_Store
.idea/
/target
*.iml
**/*.rs.bk
Cargo.lock
36 changes: 36 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
[package]
name = "fmodel_rust_postgres"
version = "1.0.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[features]
default = ["pg15"]
pg11 = ["pgrx/pg11", "pgrx-tests/pg11" ]
pg12 = ["pgrx/pg12", "pgrx-tests/pg12" ]
pg13 = ["pgrx/pg13", "pgrx-tests/pg13" ]
pg14 = ["pgrx/pg14", "pgrx-tests/pg14" ]
pg15 = ["pgrx/pg15", "pgrx-tests/pg15" ]
pg16 = ["pgrx/pg16", "pgrx-tests/pg16" ]
pg_test = []

[dependencies]
pgrx = "=0.11.4"
serde = { version = "1.0.203", features = ["derive"] }
fmodel-rust = "0.7.0"
serde_json = "1.0.117"
uuid = { version = "1.8.0", features = ["serde", "v4"] }

[dev-dependencies]
pgrx-tests = "=0.11.4"

[profile.dev]
panic = "unwind"

[profile.release]
panic = "unwind"
opt-level = 3
lto = "fat"
codegen-units = 1
67 changes: 67 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# `fmodel-rust-postgres`

Effortlessly transform your domain models into powerful PostgreSQL extensions using our GitHub repository template.
With pre-implemented infrastructure and application layers in the `framework` module, you can focus entirely on your core domain logic while running your models directly within your PostgreSQL database for seamless integration and enhanced performance.

The template includes a demo domain model of a `restaurant/order management system`, showcasing practical implementation and providing a solid foundation for your own projects.

![event model](restaurant-model.jpg)

>Actually, the domain model is copied from the traditional application [fmodel-rust-demo](https://github.com/fraktalio/fmodel-rust-demo), demonstrating how to run your unique and single domain model directly within your PostgreSQL database/`as extension`; or connect the application to the database/`traditionally`.
## Event Sourcing

With event sourcing, we delve deeper by capturing every decision or alteration as an event.
Each new transfer or modification to the account state is meticulously documented, providing a comprehensive audit trail
of all activities.
This affords you a 100% accurate historical record of your domain, enabling you to effortlessly traverse back
in time and review the state at any given moment.

**History is always on!**

## Technology
This project is using:

- [`rust` programming language](https://www.rust-lang.org/) to build a high-performance, reliable, and efficient system.
- [fmodel-rust library](https://github.com/fraktalio/fmodel-rust) to implement tactical Domain-Driven Design patterns, optimised for Event Sourcing.
- [pgrx](https://github.com/pgcentralfoundation/pgrx) to simplify the creation of custom Postgres extensions and bring `logic` closer to your data(base).

## Requirements
- [Rust](https://www.rust-lang.org/tools/install)
- [PGRX subcommand](https://github.com/pgcentralfoundation/pgrx?tab=readme-ov-file#getting-started): `cargo install --locked cargo-pgrx`
- Run `cargo pgrx init` once, to properly configure the pgrx development environment. It downloads the latest releases of supported Postgres versions, configures them for debugging, compiles them with assertions, and installs them to `"${PGRX_HOME}"`. These include all contrib extensions and tools included with Postgres. Other cargo pgrx commands such as `run` and `test` will manage and use these installations on your behalf.

> No manual Postgres database installation is required.
## Test it / Run it
Run tests:

```shell
cargo pgrx test
```

Compile/install extension to a pgrx-managed Postgres instance and start psql:
```shell
cargo pgrx run
```

Confused? Run `cargo pgrx help`

## The structure of the project

The project is structured as follows:
- `lib.rs` file contains the entry point of the package/crate.
- `framework` module contains the generalized and parametrized implementation of infrastructure and application layers.
- `domain` module contains the domain model. It is the core and pure domain logic of the application!!!
- `application` module contains the application layer. It is the orchestration of the domain model and the infrastructure layer (empty, as it is implemented in the `framework` module).
- `infrastructure` module contains the infrastructure layer / fetching and storing data (empty, as it is implemented in the `framework` module).

The framework module offers a generic implementation of the infrastructure and application layers, which can be reused across multiple domain models.
Your focus should be on the `domain` module, where you can implement your unique domain model. We have provided a demo domain model of a `restaurant/order management system` to get you started.

## References and further reading
- [pgrx](https://github.com/pgcentralfoundation/pgrx)
- [fmodel-rust](https://github.com/fraktalio/fmodel-rust)

---
Created with :heart: by [Fraktalio](https://fraktalio.com/)
5 changes: 5 additions & 0 deletions fmodel_rust_postgres.control
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
comment = 'fmodel_rust_postgres: Created by pgrx'
default_version = '@CARGO_VERSION@'
module_pathname = '$libdir/fmodel_rust_postgres'
relocatable = false
superuser = true
Binary file added restaurant-model.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
157 changes: 157 additions & 0 deletions sql/event_sourcing.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
-- ########################
-- ######## TABLES ########
-- ########################

-- Registered deciders and the respectful events that these deciders can publish (decider can publish and/or source its own state from these event types only)
CREATE TABLE IF NOT EXISTS deciders
(
-- decider name/type
"decider" TEXT NOT NULL,
-- event name/type that this decider can publish
"event" TEXT NOT NULL,
PRIMARY KEY ("decider", "event")
);

INSERT INTO deciders ("decider", "event") VALUES ('Restaurant', 'RestaurantCreated');
INSERT INTO deciders ("decider", "event") VALUES ('Restaurant', 'RestaurantNotCreated');
INSERT INTO deciders ("decider", "event") VALUES ('Restaurant', 'RestaurantMenuChanged');
INSERT INTO deciders ("decider", "event") VALUES ('Restaurant', 'RestaurantMenuNotChanged');
INSERT INTO deciders ("decider", "event") VALUES ('Restaurant', 'OrderPlaced');
INSERT INTO deciders ("decider", "event") VALUES ('Restaurant', 'OrderNotPlaced');
INSERT INTO deciders ("decider", "event") VALUES ('Order', 'OrderCreated');
INSERT INTO deciders ("decider", "event") VALUES ('Order', 'OrderPrepared');
INSERT INTO deciders ("decider", "event") VALUES ('Order', 'OrderNotCreated');
INSERT INTO deciders ("decider", "event") VALUES ('Order', 'OrderNotPrepared');


-- Events
CREATE TABLE IF NOT EXISTS events
(
-- event name/type. Part of a composite foreign key to `deciders`
"event" TEXT NOT NULL,
-- event ID. This value is used by the next event as it's `previous_id` value to guard against a Lost-EventModel problem / optimistic locking.
"event_id" UUID NOT NULL UNIQUE,
-- decider name/type. Part of a composite foreign key to `deciders`
"decider" TEXT NOT NULL,
-- business identifier for the decider
"decider_id" TEXT NOT NULL,
-- event data in JSON format
"data" JSONB NOT NULL,
-- command ID causing this event
"command_id" UUID NULL,
-- previous event uuid; null for first event; null does not trigger UNIQUE constraint; we defined a function `check_first_event_for_decider`
"previous_id" UUID UNIQUE,
-- indicator if the event stream for the `decider_id` is final
"final" BOOLEAN NOT NULL DEFAULT FALSE,
-- The timestamp of the event insertion. AUTOPOPULATES—DO NOT INSERT
"created_at" TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
-- ordering sequence/offset for all events in all deciders. AUTOPOPULATES—DO NOT INSERT
"offset" BIGSERIAL PRIMARY KEY,
FOREIGN KEY ("decider", "event") REFERENCES deciders ("decider", "event")
);


CREATE INDEX IF NOT EXISTS decider_index ON events ("decider_id", "offset");

-- ########################
-- ##### SIDE EFFECTS #####
-- ########################

-- Many things that can be done using triggers can also be implemented using the Postgres rule system.
-- What currently cannot be implemented by rules are some kinds of constraints.
-- It is possible, to place a qualified rule that rewrites a query to NOTHING if the value of a column does not appear in another table.
-- But then the data is silently thrown away, and that's not a good idea.
-- If checks for valid values are required, and in the case of an invalid value an error message should be generated, it must be done by a trigger for now.

-- SIDE EFFECT (rule): immutable decider - ignore deleting already registered events
--CREATE OR REPLACE RULE ignore_delete_decider_events AS ON DELETE TO deciders
-- DO INSTEAD NOTHING;

-- SIDE EFFECT (rule): immutable decider - ignore updating already registered events
--CREATE OR REPLACE RULE ignore_update_decider_events AS ON UPDATE TO deciders
-- DO INSTEAD NOTHING;

-- SIDE EFFECT (rule): immutable events - ignore delete
CREATE OR REPLACE RULE ignore_delete_events AS ON DELETE TO events
DO INSTEAD NOTHING;

-- SIDE EFFECT (rule): immutable events - ignore update
CREATE OR REPLACE RULE ignore_update_events AS ON UPDATE TO events
DO INSTEAD NOTHING;


-- SIDE EFFECT (trigger): Can only use null previousId for first event in an decider
CREATE OR REPLACE FUNCTION check_first_event_for_decider() RETURNS trigger AS
'
BEGIN
IF (NEW.previous_id IS NULL
AND EXISTS(SELECT 1
FROM events
WHERE NEW.decider_id = decider_id
AND NEW.decider = decider))
THEN
RAISE EXCEPTION ''previous_id can only be null for first decider event'';
END IF;
RETURN NEW;
END;
'
LANGUAGE plpgsql;

DROP TRIGGER IF EXISTS t_check_first_event_for_decider ON events;
CREATE TRIGGER t_check_first_event_for_decider
BEFORE INSERT
ON events
FOR EACH ROW
EXECUTE FUNCTION check_first_event_for_decider();


-- SIDE EFFECT (trigger): can only append events if the decider_id stream is not finalized already
CREATE OR REPLACE FUNCTION check_final_event_for_decider() RETURNS trigger AS
'
BEGIN
IF EXISTS(SELECT 1
FROM events
WHERE NEW.decider_id = decider_id
AND "final" = TRUE
AND NEW.decider = decider)
THEN
RAISE EXCEPTION ''last event for this decider stream is already final. the stream is closed, you can not append events to it.'';
END IF;
RETURN NEW;
END;
'
LANGUAGE plpgsql;

DROP TRIGGER IF EXISTS t_check_final_event_for_decider ON events;
CREATE TRIGGER t_check_final_event_for_decider
BEFORE INSERT
ON events
FOR EACH ROW
EXECUTE FUNCTION check_final_event_for_decider();


-- SIDE EFFECT (trigger): previousId must be in the same decider as the event
CREATE OR REPLACE FUNCTION check_previous_id_in_same_decider() RETURNS trigger AS
'
BEGIN
IF (NEW.previous_id IS NOT NULL
AND NOT EXISTS(SELECT 1
FROM events
WHERE NEW.previous_id = event_id
AND NEW.decider_id = decider_id
AND NEW.decider = decider))
THEN
RAISE EXCEPTION ''previous_id must be in the same decider'';
END IF;
RETURN NEW;
END;
'
LANGUAGE plpgsql;

DROP TRIGGER IF EXISTS t_check_previous_id_in_same_decider ON events;
CREATE TRIGGER t_check_previous_id_in_same_decider
BEFORE INSERT
ON events
FOR EACH ROW
EXECUTE FUNCTION check_previous_id_in_same_decider();

1 change: 1 addition & 0 deletions src/application/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod order_restaurant_aggregate;
15 changes: 15 additions & 0 deletions src/application/order_restaurant_aggregate.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
use crate::domain::order_decider::Order;
use crate::framework::application::event_sourced_aggregate::EventSourcedOrchestratingAggregate;

use crate::domain::restaurant_decider::Restaurant;
use crate::domain::{Command, Event};
use crate::infrastructure::order_restaurant_event_repository::OrderAndRestaurantEventRepository;

/// A convenient type alias for the order and restaurant aggregate.
pub type OrderAndRestaurantAggregate<'a> = EventSourcedOrchestratingAggregate<
'a,
Command,
(Option<Restaurant>, Option<Order>),
Event,
OrderAndRestaurantEventRepository,
>;
Loading

0 comments on commit 790d2e1

Please sign in to comment.