-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 790d2e1
Showing
29 changed files
with
1,958 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
.DS_Store | ||
.idea/ | ||
/target | ||
*.iml | ||
**/*.rs.bk | ||
Cargo.lock |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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/) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
pub mod order_restaurant_aggregate; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
>; |
Oops, something went wrong.