Separate command and query

This commit is contained in:
Jon Staab
2026-04-01 15:33:03 -07:00
parent baae65b8b2
commit 07dfe86210
18 changed files with 615 additions and 549 deletions
+13 -13
View File
@@ -5,10 +5,10 @@ Api manages the HTTP interface for the application
Members:
- `host: String` - the hostname of the service for checking NIP 98 auth, from `HOST`
- `port: u16` - a port to run the server on from `PORT`
- `admins: Vec<String>` - a list of admin pubkeys from `ADMINS`
- `origins: Vec<String>` - to be used in CORS headers, from `ALLOW_ORIGINS`
- `repo: Repo`
- `query: Query`
- `command: Command`
- `billing: Billing`
Notes:
@@ -31,7 +31,7 @@ Notes:
- Serves `GET /plans`
- No authentication required
- Return `data` is a list of plan structs from `Repo::list_plans`
- Return `data` is a list of plan structs from `Query::list_plans`
## `async fn get_plan(...) -> Response`
@@ -55,26 +55,26 @@ Notes:
- Serves `GET /tenants`
- Authorizes admin only
- Return `data` is a list of tenant structs from `repo.list_tenants`
- Return `data` is a list of tenant structs from `query.list_tenants`
## `async fn get_tenant(...) -> Response`
- Serves `GET /tenants/:pubkey`
- Authorizes admin or matching tenant
- Return `data` is a single tenant struct from `repo.get_tenant`
- Return `data` is a single tenant struct from `query.get_tenant`
## `async fn update_tenant(...) -> Response`
- Serves `PUT /tenants/:pubkey`
- Authorizes admin or matching tenant
- Updates tenant using `repo.update_tenant`
- Updates tenant using `command.update_tenant`
- Return `data` is the updated tenant struct
## `async fn list_tenant_relays(...) -> Response`
- Serves `GET /tenants/:pubkey/relays`
- Authorizes admin or matching tenant
- Return `data` is a list of relay structs from `repo.list_relays_for_tenant`
- Return `data` is a list of relay structs from `query.list_relays_for_tenant`
--- Relay routes
@@ -82,20 +82,20 @@ Notes:
- Serves `GET /relays`
- Authorizes admin only
- Return `data` is a list of relay structs from `repo.list_relays`
- Return `data` is a list of relay structs from `query.list_relays`
## `async fn get_relay(...) -> Response`
- Serves `GET /relays/:id`
- Authorizes admin or relay owner
- Return `data` is a single relay struct from `repo.get_relay`
- Return `data` is a single relay struct from `query.get_relay`
## `async fn create_relay(...) -> Response`
- Serves `POST /relays`
- Authorizes admin or matching tenant pubkey in request body
- Validates/prepares the relay data to be saved using `prepare_relay`
- Creates a new relay using `repo.create_relay`
- Creates a new relay using `command.create_relay`
- If relay is a duplicate by subdomain, return a `422` with `code=subdomain-exists`
- Return `data` is a single relay struct. Use HTTP `201`.
@@ -104,7 +104,7 @@ Notes:
- Serves `PUT /relays/:id`
- Authorizes admin or relay owner
- Validates/prepares the relay data to be saved using `prepare_relay`
- Updates the given relay using `repo.update_relay`
- Updates the given relay using `command.update_relay`
- If relay is a duplicate by subdomain, return a `422` with `code=subdomain-exists`
- Return `data` is a single relay struct.
@@ -112,7 +112,7 @@ Notes:
- Serves `GET /relays/:id/activity`
- Authorizes admin or relay owner
- Get activity from `repo.list_activity_for_relay`
- Get activity from `query.list_activity_for_relay`
- Return `data` is `{activity}`
## `async fn deactivate_relay(...) -> Response`
+3 -2
View File
@@ -5,9 +5,10 @@ Billing encapsulates logic related to synchronizing state with Stripe.
Members:
- `nwc_url: String` - a nostr wallet connect URL used to **create** bolt11 invoices
- `repo: Repo`
- `query: Query`
- `command: Command`
- `robot: Robot`
## `pub fn new(repo: Repo, robot: Robot) -> Self`
## `pub fn new(query: Query, command: Command, robot: Robot) -> Self`
- Reads environment and populates members
+57
View File
@@ -0,0 +1,57 @@
# `pub struct Command`
Command writes to the database.
Members:
- `pool: SqlitePool` - a sqlite connection pool
Notes:
- All public write methods should be atomic
- All writes should be accompanied by an activity log entry of `(tenant, activity_type, resource_type, resource_id)`
## `pub fn new(&self, pool: SqlitePool) -> Self`
- Assigns pool to self
## `pub fn create_tenant(&self, tenant: &Tenant) -> Result<()>`
- Creates tenant, may throw sqlite uniqueness error on pubkey
- Logs activity as `(create_tenant, tenant_id)`
## `pub fn update_tenant(&self, tenant: &Tenant) -> Result<()>`
- Updates tenant
- Logs activity as `(update_tenant, tenant_id)`
## `pub fn create_relay(&self, relay: &Relay) -> Result<()>`
- Creates relay, may throw sqlite uniqueness error on subdomain
- Sets relay status to `new`
- Logs activity as `(create_relay, relay_id)`
## `pub fn update_relay(&self, relay: &Relay) -> Result<()>`
- Updates relay, may throw sqlite uniqueness error on subdomain
- Logs activity as `(update_relay, relay_id)`
## `pub fn deactivate_relay(&self, relay: &Relay) -> Result<()>`
- Sets relay status to `inactive`
- Logs activity as `(deactivate_relay, relay_id)`
## `pub fn activate_relay(&self, relay: &Relay) -> Result<()>`
- Sets relay status to `active`
- Logs activity as `(activate_relay, relay_id)`
## `pub fn fail_relay_sync(&self, relay: &Relay, sync_error: &str) -> Result<()>`
- Sets relay status to `inactive`, sets `sync_error`
- Logs activity as `(fail_relay_sync, relay_id)`
## `pub fn mark_relay_synced(&self, relay_id: &str) -> Result<()>`
- Sets `synced = 1`, `status = 'active'`, clears `sync_error`
- No activity log (called by infra after successful sync)
+7 -6
View File
@@ -5,25 +5,26 @@ Infra is a service which polls the database and synchronizes updates to relays t
Members:
- `api_url: String` - the URL of the zooid instance to be managed, from `ZOOID_API_URL`
- `repo: Repo`
- `query: Query`
- `command: Command`
## `pub fn new(repo: Repo) -> Self`
## `pub fn new(query: Query, command: Command) -> Self`
- Reads environment and populates members
## `pub async fn start(self)`
- Initializes `last_activity_at` from `repo.max_activity_at()` so historical activities are not replayed on restart.
- Initializes `last_activity_at` from `query.max_activity_at()` so historical activities are not replayed on restart.
- Calls `self.tick` in a loop every 10 seconds.
## `pub async fn tick(self)`
Iterates over `repo.list_activity` since last run and does the following:
Iterates over `query.list_activity` since last run and does the following:
- For `create_relay`, `update_relay`, or `deactivate_relay` activity, sync the relay to zooid.
- Uses `relay.synced` to decide POST vs PUT (not the activity type), so already-synced relays always use PUT even on restart.
- On success, calls `repo.mark_relay_synced` to set `synced = 1`, `status = 'active'`, and clear `sync_error`.
- On failure, calls `repo.fail_relay_sync`.
- On success, calls `command.mark_relay_synced` to set `synced = 1`, `status = 'active'`, and clear `sync_error`.
- On failure, calls `command.fail_relay_sync`.
- All other activity types are ignored (e.g. `fail_relay_sync` must not trigger another sync).
## `async fn sync_relay(&self, relay: &Relay, is_new: bool)`
+2 -2
View File
@@ -1,8 +1,8 @@
# `async fn main() -> Result<()>`
- Configures logging
- Creates instances of `Repo`, `Robot`, `Billing`, `Api`, and `Infra`
- Spawns `infra.start`
- Calls `create_pool` to get a `SqlitePool`, then creates `Query`, `Command`, `Robot`, `Billing`, `Api`, and `Infra`
- Get an axum router from `api.router`
- Adds CORS middleware based on `origins`
- Calls `axum::serve` with a listener
- Spawns `infra.start`
+14
View File
@@ -0,0 +1,14 @@
# `pub async fn create_pool() -> Result<SqlitePool>`
Creates and returns a sqlite connection pool.
Notes:
- Database table names are singular: `activity`, `tenant`, `relay`
Steps:
- Reads `DATABASE_URL` from environment
- Ensures that any directories referred to in `DATABASE_URL` exist
- Initializes the sqlx pool
- Runs migrations found in the `migrations` directory
+50
View File
@@ -0,0 +1,50 @@
# `pub struct Query`
Query reads from the database.
Members:
- `pool: SqlitePool` - a sqlite connection pool
## `pub fn new(&self, pool: SqlitePool) -> Self`
- Assigns pool to self
## `pub fn list_tenants(&self) -> Result<Vec<Tenant>>`
- Returns all tenants
## `pub fn get_tenant(&self, pubkey: &str) -> Result<Tenant>`
- Returns matching tenant
## `pub fn list_plans() -> Vec<Plan>`
- Returns the hardcoded relay plans used by the system (`free`, `basic`, `growth`)
- This is the source of truth for plan metadata exposed via API
## `pub fn list_relays(&self) -> Result<Vec<Relay>>`
- Returns all relays
## `pub fn list_relays_for_tenant(&self, tenant_id: &str) -> Result<Vec<Relay>>`
- Returns all relays belonging to the given tenant
## `pub fn get_relay(&self, id: &str) -> Result<Relay>`
- Returns matching relay
## `pub fn max_activity_at(&self) -> Result<i64>`
- Returns the maximum `created_at` value from the activity table, or 0 if empty
- Used by infra to initialize the since guard on startup
## `pub fn list_activity(&self, since: &i64) -> Result<Vec<Activity>>`
- Returns all activity occuring after `since`
## `pub fn list_activity_for_relay(&self, relay_id: &str) -> Result<Vec<Activity>>`
- Returns all activity where `resource_type = 'relay'` and `resource_id = relay_id`
- Ordered newest-first
-107
View File
@@ -1,107 +0,0 @@
# `pub struct Repo`
Repo is a wrapper around a sqlite pool which implements methods related to database access.
Members:
- `pool: sqlx::SqlitePool` - a sqlite connection pool
Notes:
- All public write methods should be run in a transaction so they're atomic
- All writes should be accompanied by an activity log entry of `(tenant, activity_type, resource_type, resource_id)`
- Database table names are singular: `activity`, `tenant`, `relay`
## `pub fn new() -> Self`
- Reads `DATABASE_URL` from environment
- Ensures that any directories referred to in `DATABASE_URL` exist
- Initializes its sqlx `pool`
- Runs migrations found in the `migrations` directory.
## `fn insert_activity(activity_type, resource_type, resource_id) -> Result<()>`
- Private helper that inserts one row into `activity`
- Infers `tenant` from `resource_type` and `resource_id`
- Used by write methods to avoid repeating audit-log SQL
## `pub fn list_tenants(&self) -> Result<Vec<Tenant>>`
- Returns all tenants
## `pub fn get_tenant(&self, pubkey: &str) -> Result<Tenant>`
- Returns matching tenant
## `pub fn create_tenant(&self, tenant: &Tenant) -> Result<()>`
- Creates tenant, may throw sqlite uniqueness error on pubkey
- Logs activity as `(create_tenant, tenant_id)`
## `pub fn update_tenant(&self, tenant: &Tenant) -> Result<()>`
- Updates tenant
- Logs activity as `(update_tenant, tenant_id)`
## `pub fn list_plans() -> Vec<Plan>`
- Returns the hardcoded relay plans used by the system (`free`, `basic`, `growth`)
- This is the source of truth for plan metadata exposed via API
## `pub fn list_relays(&self) -> Result<Vec<Relay>>`
- Returns all relays
## `pub fn list_relays_for_tenant(&self, tenant_id: &str) -> Result<Vec<Relay>>`
- Returns all relays belonging to the given tenant
## `pub fn get_relay(&self, id: &str) -> Result<Relay>`
- Returns matching relay
## `pub fn create_relay(&self, relay: &Relay) -> Result<()>`
- Creates relay, may throw sqlite uniqueness error on subdomain
- Sets relay status to `new`
- Logs activity as `(create_relay, relay_id)`
## `pub fn update_relay(&self, relay: &Relay) -> Result<()>`
- Updates relay, may throw sqlite uniqueness error on subdomain
- Logs activity as `(update_relay, relay_id)`
## `pub fn deactivate_relay(&self, relay: &Relay) -> Result<()>`
- Sets relay status to `inactive`
- Logs activity as `(deactivate_relay, relay_id)`
## `pub fn activate_relay(&self, relay: &Relay) -> Result<()>`
- Sets relay status to `active`
- Logs activity as `(activate_relay, relay_id)`
## `pub fn fail_relay_sync(&self, relay: &Relay, sync_error: &str) -> Result<()>`
- Sets relay status to `inactive`, sets `sync_error`
- Logs activity as `(fail_relay_sync, relay_id)`
## `pub fn mark_relay_synced(&self, relay_id: &str) -> Result<()>`
- Sets `synced = 1`, `status = 'active'`, clears `sync_error`
- No activity log (called by infra after successful sync)
## `pub fn max_activity_at(&self) -> Result<i64>`
- Returns the maximum `created_at` value from the activity table, or 0 if empty
- Used by infra to initialize the since guard on startup
## `pub fn list_activity(&self, since: &i64) -> Result<Vec<Activity>>`
- Returns all activity occuring after `since`
## `pub fn list_activity_for_relay(&self, relay_id: &str) -> Result<Vec<Activity>>`
- Returns all activity where `resource_type = 'relay'` and `resource_id = relay_id`
- Ordered newest-first