Skip to content

Commit 790d2e1

Browse files
committed
first commit
0 parents  commit 790d2e1

29 files changed

+1958
-0
lines changed

.cargo/config.toml

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[target.'cfg(target_os="macos")']
2+
# Postgres symbols won't be available until runtime
3+
rustflags = ["-Clink-arg=-Wl,-undefined,dynamic_lookup"]

.github/workflows/lint-and-test.yml

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
name: 🧪 Lint and Test
2+
3+
on:
4+
push:
5+
branches-ignore: [wip/**]
6+
7+
jobs:
8+
test:
9+
runs-on: ubuntu-latest
10+
container: pgxn/pgxn-tools
11+
strategy:
12+
matrix:
13+
pg: [11, 12, 13, 14, 15, 16]
14+
name: 🐘 Postgres ${{ matrix.pg }}
15+
steps:
16+
- name: Checkout
17+
uses: actions/checkout@v4
18+
- name: Start PostgreSQL ${{ matrix.pg }}
19+
run: pg-start ${{ matrix.pg }}
20+
- name: Setup Rust Cache
21+
uses: Swatinem/rust-cache@v2
22+
- name: Test on PostgreSQL ${{ matrix.pg }}
23+
run: pgrx-build-test
24+
25+
lint:
26+
name: ✅ Lint and Cover
27+
runs-on: ubuntu-latest
28+
container: pgxn/pgxn-tools
29+
env: { PGVERSION: 16 }
30+
steps:
31+
- name: Checkout
32+
uses: actions/checkout@v4
33+
- name: Start PostgreSQL ${{ env.PGVERSION }}
34+
run: pg-start ${{ env.PGVERSION }} libxml2-utils
35+
- name: Setup Rust Cache
36+
uses: Swatinem/rust-cache@v2
37+
- name: Install pgrx
38+
run: make install-pgrx
39+
- name: Initialize pgrx
40+
run: make pgrx-init
41+
- name: Format and Lint
42+
run: make lint

.gitignore

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
.DS_Store
2+
.idea/
3+
/target
4+
*.iml
5+
**/*.rs.bk
6+
Cargo.lock

Cargo.toml

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
[package]
2+
name = "fmodel_rust_postgres"
3+
version = "1.0.0"
4+
edition = "2021"
5+
6+
[lib]
7+
crate-type = ["cdylib"]
8+
9+
[features]
10+
default = ["pg15"]
11+
pg11 = ["pgrx/pg11", "pgrx-tests/pg11" ]
12+
pg12 = ["pgrx/pg12", "pgrx-tests/pg12" ]
13+
pg13 = ["pgrx/pg13", "pgrx-tests/pg13" ]
14+
pg14 = ["pgrx/pg14", "pgrx-tests/pg14" ]
15+
pg15 = ["pgrx/pg15", "pgrx-tests/pg15" ]
16+
pg16 = ["pgrx/pg16", "pgrx-tests/pg16" ]
17+
pg_test = []
18+
19+
[dependencies]
20+
pgrx = "=0.11.4"
21+
serde = { version = "1.0.203", features = ["derive"] }
22+
fmodel-rust = "0.7.0"
23+
serde_json = "1.0.117"
24+
uuid = { version = "1.8.0", features = ["serde", "v4"] }
25+
26+
[dev-dependencies]
27+
pgrx-tests = "=0.11.4"
28+
29+
[profile.dev]
30+
panic = "unwind"
31+
32+
[profile.release]
33+
panic = "unwind"
34+
opt-level = 3
35+
lto = "fat"
36+
codegen-units = 1

README.md

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# `fmodel-rust-postgres`
2+
3+
Effortlessly transform your domain models into powerful PostgreSQL extensions using our GitHub repository template.
4+
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.
5+
6+
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.
7+
8+
![event model](restaurant-model.jpg)
9+
10+
>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`.
11+
12+
## Event Sourcing
13+
14+
With event sourcing, we delve deeper by capturing every decision or alteration as an event.
15+
Each new transfer or modification to the account state is meticulously documented, providing a comprehensive audit trail
16+
of all activities.
17+
This affords you a 100% accurate historical record of your domain, enabling you to effortlessly traverse back
18+
in time and review the state at any given moment.
19+
20+
**History is always on!**
21+
22+
## Technology
23+
This project is using:
24+
25+
- [`rust` programming language](https://www.rust-lang.org/) to build a high-performance, reliable, and efficient system.
26+
- [fmodel-rust library](https://github.com/fraktalio/fmodel-rust) to implement tactical Domain-Driven Design patterns, optimised for Event Sourcing.
27+
- [pgrx](https://github.com/pgcentralfoundation/pgrx) to simplify the creation of custom Postgres extensions and bring `logic` closer to your data(base).
28+
29+
## Requirements
30+
- [Rust](https://www.rust-lang.org/tools/install)
31+
- [PGRX subcommand](https://github.com/pgcentralfoundation/pgrx?tab=readme-ov-file#getting-started): `cargo install --locked cargo-pgrx`
32+
- 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.
33+
34+
> No manual Postgres database installation is required.
35+
36+
## Test it / Run it
37+
Run tests:
38+
39+
```shell
40+
cargo pgrx test
41+
```
42+
43+
Compile/install extension to a pgrx-managed Postgres instance and start psql:
44+
```shell
45+
cargo pgrx run
46+
```
47+
48+
Confused? Run `cargo pgrx help`
49+
50+
## The structure of the project
51+
52+
The project is structured as follows:
53+
- `lib.rs` file contains the entry point of the package/crate.
54+
- `framework` module contains the generalized and parametrized implementation of infrastructure and application layers.
55+
- `domain` module contains the domain model. It is the core and pure domain logic of the application!!!
56+
- `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).
57+
- `infrastructure` module contains the infrastructure layer / fetching and storing data (empty, as it is implemented in the `framework` module).
58+
59+
The framework module offers a generic implementation of the infrastructure and application layers, which can be reused across multiple domain models.
60+
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.
61+
62+
## References and further reading
63+
- [pgrx](https://github.com/pgcentralfoundation/pgrx)
64+
- [fmodel-rust](https://github.com/fraktalio/fmodel-rust)
65+
66+
---
67+
Created with :heart: by [Fraktalio](https://fraktalio.com/)

fmodel_rust_postgres.control

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
comment = 'fmodel_rust_postgres: Created by pgrx'
2+
default_version = '@CARGO_VERSION@'
3+
module_pathname = '$libdir/fmodel_rust_postgres'
4+
relocatable = false
5+
superuser = true

restaurant-model.jpg

269 KB
Loading

sql/event_sourcing.sql

+157
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
-- ########################
2+
-- ######## TABLES ########
3+
-- ########################
4+
5+
-- 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)
6+
CREATE TABLE IF NOT EXISTS deciders
7+
(
8+
-- decider name/type
9+
"decider" TEXT NOT NULL,
10+
-- event name/type that this decider can publish
11+
"event" TEXT NOT NULL,
12+
PRIMARY KEY ("decider", "event")
13+
);
14+
15+
INSERT INTO deciders ("decider", "event") VALUES ('Restaurant', 'RestaurantCreated');
16+
INSERT INTO deciders ("decider", "event") VALUES ('Restaurant', 'RestaurantNotCreated');
17+
INSERT INTO deciders ("decider", "event") VALUES ('Restaurant', 'RestaurantMenuChanged');
18+
INSERT INTO deciders ("decider", "event") VALUES ('Restaurant', 'RestaurantMenuNotChanged');
19+
INSERT INTO deciders ("decider", "event") VALUES ('Restaurant', 'OrderPlaced');
20+
INSERT INTO deciders ("decider", "event") VALUES ('Restaurant', 'OrderNotPlaced');
21+
INSERT INTO deciders ("decider", "event") VALUES ('Order', 'OrderCreated');
22+
INSERT INTO deciders ("decider", "event") VALUES ('Order', 'OrderPrepared');
23+
INSERT INTO deciders ("decider", "event") VALUES ('Order', 'OrderNotCreated');
24+
INSERT INTO deciders ("decider", "event") VALUES ('Order', 'OrderNotPrepared');
25+
26+
27+
-- Events
28+
CREATE TABLE IF NOT EXISTS events
29+
(
30+
-- event name/type. Part of a composite foreign key to `deciders`
31+
"event" TEXT NOT NULL,
32+
-- 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.
33+
"event_id" UUID NOT NULL UNIQUE,
34+
-- decider name/type. Part of a composite foreign key to `deciders`
35+
"decider" TEXT NOT NULL,
36+
-- business identifier for the decider
37+
"decider_id" TEXT NOT NULL,
38+
-- event data in JSON format
39+
"data" JSONB NOT NULL,
40+
-- command ID causing this event
41+
"command_id" UUID NULL,
42+
-- previous event uuid; null for first event; null does not trigger UNIQUE constraint; we defined a function `check_first_event_for_decider`
43+
"previous_id" UUID UNIQUE,
44+
-- indicator if the event stream for the `decider_id` is final
45+
"final" BOOLEAN NOT NULL DEFAULT FALSE,
46+
-- The timestamp of the event insertion. AUTOPOPULATES—DO NOT INSERT
47+
"created_at" TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
48+
-- ordering sequence/offset for all events in all deciders. AUTOPOPULATES—DO NOT INSERT
49+
"offset" BIGSERIAL PRIMARY KEY,
50+
FOREIGN KEY ("decider", "event") REFERENCES deciders ("decider", "event")
51+
);
52+
53+
54+
CREATE INDEX IF NOT EXISTS decider_index ON events ("decider_id", "offset");
55+
56+
-- ########################
57+
-- ##### SIDE EFFECTS #####
58+
-- ########################
59+
60+
-- Many things that can be done using triggers can also be implemented using the Postgres rule system.
61+
-- What currently cannot be implemented by rules are some kinds of constraints.
62+
-- 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.
63+
-- But then the data is silently thrown away, and that's not a good idea.
64+
-- 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.
65+
66+
-- SIDE EFFECT (rule): immutable decider - ignore deleting already registered events
67+
--CREATE OR REPLACE RULE ignore_delete_decider_events AS ON DELETE TO deciders
68+
-- DO INSTEAD NOTHING;
69+
70+
-- SIDE EFFECT (rule): immutable decider - ignore updating already registered events
71+
--CREATE OR REPLACE RULE ignore_update_decider_events AS ON UPDATE TO deciders
72+
-- DO INSTEAD NOTHING;
73+
74+
-- SIDE EFFECT (rule): immutable events - ignore delete
75+
CREATE OR REPLACE RULE ignore_delete_events AS ON DELETE TO events
76+
DO INSTEAD NOTHING;
77+
78+
-- SIDE EFFECT (rule): immutable events - ignore update
79+
CREATE OR REPLACE RULE ignore_update_events AS ON UPDATE TO events
80+
DO INSTEAD NOTHING;
81+
82+
83+
-- SIDE EFFECT (trigger): Can only use null previousId for first event in an decider
84+
CREATE OR REPLACE FUNCTION check_first_event_for_decider() RETURNS trigger AS
85+
'
86+
BEGIN
87+
IF (NEW.previous_id IS NULL
88+
AND EXISTS(SELECT 1
89+
FROM events
90+
WHERE NEW.decider_id = decider_id
91+
AND NEW.decider = decider))
92+
THEN
93+
RAISE EXCEPTION ''previous_id can only be null for first decider event'';
94+
END IF;
95+
RETURN NEW;
96+
END;
97+
'
98+
LANGUAGE plpgsql;
99+
100+
DROP TRIGGER IF EXISTS t_check_first_event_for_decider ON events;
101+
CREATE TRIGGER t_check_first_event_for_decider
102+
BEFORE INSERT
103+
ON events
104+
FOR EACH ROW
105+
EXECUTE FUNCTION check_first_event_for_decider();
106+
107+
108+
-- SIDE EFFECT (trigger): can only append events if the decider_id stream is not finalized already
109+
CREATE OR REPLACE FUNCTION check_final_event_for_decider() RETURNS trigger AS
110+
'
111+
BEGIN
112+
IF EXISTS(SELECT 1
113+
FROM events
114+
WHERE NEW.decider_id = decider_id
115+
AND "final" = TRUE
116+
AND NEW.decider = decider)
117+
THEN
118+
RAISE EXCEPTION ''last event for this decider stream is already final. the stream is closed, you can not append events to it.'';
119+
END IF;
120+
RETURN NEW;
121+
END;
122+
'
123+
LANGUAGE plpgsql;
124+
125+
DROP TRIGGER IF EXISTS t_check_final_event_for_decider ON events;
126+
CREATE TRIGGER t_check_final_event_for_decider
127+
BEFORE INSERT
128+
ON events
129+
FOR EACH ROW
130+
EXECUTE FUNCTION check_final_event_for_decider();
131+
132+
133+
-- SIDE EFFECT (trigger): previousId must be in the same decider as the event
134+
CREATE OR REPLACE FUNCTION check_previous_id_in_same_decider() RETURNS trigger AS
135+
'
136+
BEGIN
137+
IF (NEW.previous_id IS NOT NULL
138+
AND NOT EXISTS(SELECT 1
139+
FROM events
140+
WHERE NEW.previous_id = event_id
141+
AND NEW.decider_id = decider_id
142+
AND NEW.decider = decider))
143+
THEN
144+
RAISE EXCEPTION ''previous_id must be in the same decider'';
145+
END IF;
146+
RETURN NEW;
147+
END;
148+
'
149+
LANGUAGE plpgsql;
150+
151+
DROP TRIGGER IF EXISTS t_check_previous_id_in_same_decider ON events;
152+
CREATE TRIGGER t_check_previous_id_in_same_decider
153+
BEFORE INSERT
154+
ON events
155+
FOR EACH ROW
156+
EXECUTE FUNCTION check_previous_id_in_same_decider();
157+

src/application/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pub mod order_restaurant_aggregate;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
use crate::domain::order_decider::Order;
2+
use crate::framework::application::event_sourced_aggregate::EventSourcedOrchestratingAggregate;
3+
4+
use crate::domain::restaurant_decider::Restaurant;
5+
use crate::domain::{Command, Event};
6+
use crate::infrastructure::order_restaurant_event_repository::OrderAndRestaurantEventRepository;
7+
8+
/// A convenient type alias for the order and restaurant aggregate.
9+
pub type OrderAndRestaurantAggregate<'a> = EventSourcedOrchestratingAggregate<
10+
'a,
11+
Command,
12+
(Option<Restaurant>, Option<Order>),
13+
Event,
14+
OrderAndRestaurantEventRepository,
15+
>;

0 commit comments

Comments
 (0)