Files
portal/src/aggregates/project.rs
T
Bendik Aagaard LynghaugandClaude Sonnet 5 ebf4bf91b3
Deploy / deploy (push) Failing after 3s
Event-sourced applicant/subscriber/project aggregates, generalized resources
Replaces the free-string, direct-KV-mutate state model in answers.rs
with a proper event log (events/store.rs, JetStream-backed, CAS via
expected_last_subject_sequence) and three pure state machines
(aggregates/{applicant,subscriber,project}.rs). Closes a real
lost-update race in the old transition_answer (concurrent decisions on
the same item could both win, publishing contradictory events). KV
buckets become best-effort read-model projections, not the source of
truth. Content-declared transition targets are now validated at
load/reload time against the real compiled transition tables, not
accepted as arbitrary strings.

Buckets renamed to describe their content, not their relation to the
app (portal_applicants -> applicants, etc); "inquiry" folded into a
richer "project" concept.

ResourceSpec generalized beyond a single KV bucket: Kv | GiteaStarred |
GiteaOrgRepos | Url sources, with an optional jq filter (via the jaq
crate) to shape live data for the frontend. Url source is SSRF-guarded
(https-only, rejects loopback/private/link-local, real DNS resolve).

New headless question_lint binary (validates content against compiled
transition tables with no NATS/OIDC/server involved) and a one-time
backfill_events binary (dry-run by default) for migrating existing KV
data onto the new event log.

Questions get an optional `responsible` contact plus a lightweight
"report this question" action.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-06 08:52:29 +02:00

44 lines
1.3 KiB
Rust

//! Project submissions (`record_as: projects` - absorbs what used to be
//! called "inquiries"). A visitor submits a project idea plus who it's
//! for (org/contact details, captured as plain payload fields - no
//! separate Organization aggregate yet, see the event-sourcing plan).
//! `Open -> Accepted | Declined` - a real b2b-pipeline pair of terminal
//! states rather than the old generic `Handled`.
use super::AggregateKind;
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum State {
Open,
Accepted,
Declined,
}
impl AggregateKind for State {
const AGGREGATE_TYPE: &'static str = "project";
const INITIAL_STATE: Self = State::Open;
fn from_event_type(event_type: &str) -> Option<Self> {
match event_type {
"submitted" => Some(State::Open),
"accepted" => Some(State::Accepted),
"declined" => Some(State::Declined),
_ => None,
}
}
fn event_type(self) -> &'static str {
match self {
State::Open => "submitted",
State::Accepted => "accepted",
State::Declined => "declined",
}
}
fn allowed(self) -> &'static [Self] {
match self {
State::Open => &[State::Accepted, State::Declined],
State::Accepted | State::Declined => &[],
}
}
}