use crate::access_control::{
AccessLevel, CommandToken, DirectCreationToken, EventReadStartPosition, EventReadToken,
ExternalEventToken, PrivateFieldGrant, Role, RoleAccessMapping, RoleStatus, TokenStatus,
};
use crate::blind::BlindIndex;
use crate::encryption::DataKey;
use crate::error::SkiljRejection;
use crate::shared::{
CommandDecision, Filter, FilterOperator, Metadata, PrivateField, PrivateFieldKind, QueryItem,
QueryItemMapping, SensitiveField, Tag, TagMapping,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum BoundedContextStatus {
Active,
Archived,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BoundedContext {
pub name: String,
pub status: BoundedContextStatus,
pub created_at: chrono::DateTime<chrono::Utc>,
pub created_by: crate::bootstrap::ContextCreator,
pub template: Option<Box<BoundedContext>>,
}
impl BoundedContext {
pub fn same_as(&self, other: &BoundedContext) -> bool {
self.name == other.name
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum MissedOccurrencePolicy {
Skip,
FireOnce,
ReplayBacklog,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EventType {
pub bounded_context: BoundedContext,
pub name: String,
pub schema: String,
pub schema_version: i64,
pub tag_mappings: Vec<TagMapping>,
pub owner_tag_key: Option<String>,
pub sensitive_fields: Vec<SensitiveField>,
pub private_fields: Vec<PrivateField>,
pub external_creation_allowed: bool,
pub direct_creation_allowed: bool,
pub system_triggered_allowed: bool,
pub system_triggered_schedule: Option<String>,
pub missed_occurrence_policy: Option<MissedOccurrencePolicy>,
pub schedule_position: Option<chrono::DateTime<chrono::Utc>>,
pub last_fired_at: Option<chrono::DateTime<chrono::Utc>>,
pub event_read_allowed: bool,
}
impl EventType {
pub fn same_as(&self, other: &EventType) -> bool {
self.name == other.name && self.bounded_context.same_as(&other.bounded_context)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CommandType {
pub bounded_context: BoundedContext,
pub name: String,
pub schema: String,
pub schema_version: i64,
pub tag_mappings: Vec<TagMapping>,
pub owner_tag_key: Option<String>,
pub sensitive_fields: Vec<SensitiveField>,
pub private_fields: Vec<PrivateField>,
pub rest_trigger_allowed: bool,
}
impl CommandType {
pub fn same_as(&self, other: &CommandType) -> bool {
self.name == other.name && self.bounded_context.same_as(&other.bounded_context)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum EncryptionKeyStatus {
Active,
Destroyed,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EncryptionKey {
pub bounded_context: BoundedContext,
pub subject_key: String,
pub subject_value: String,
pub status: EncryptionKeyStatus,
pub created_at: chrono::DateTime<chrono::Utc>,
pub destroyed_at: Option<chrono::DateTime<chrono::Utc>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProtectedPayload {
pub payload: String,
pub encryption_keys: Vec<EncryptionKey>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Command {
pub id: String,
pub bounded_context: BoundedContext,
pub command_type: CommandType,
pub payload: String,
pub metadata: Metadata,
pub encryption_keys: Vec<EncryptionKey>,
pub consistency_tags: Vec<Tag>,
pub consistency_query: Vec<QueryItem>,
pub consistency_boundary: Option<i64>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum EventOrigin {
CommandTriggered {
command: Box<Command>,
},
ExternalTriggered {
source_content: String,
source_context: Option<String>,
},
DirectlyCreated,
SystemTriggered,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Event {
pub bounded_context: BoundedContext,
pub event_type: EventType,
pub payload: String,
pub metadata: Metadata,
pub sequence: i64,
pub tags: Vec<Tag>,
pub encryption_keys: Vec<EncryptionKey>,
pub origin: EventOrigin,
}
pub fn event_causation_id(event: &Event) -> String {
format!("{}:{}", event.bounded_context.name, event.sequence)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AllEventsSubscription {
pub bounded_context: BoundedContext,
pub access_mapping: RoleAccessMapping,
pub created_at: chrono::DateTime<chrono::Utc>,
pub from_sequence: i64,
pub event_types: Vec<EventType>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EventTypeSubscription {
pub bounded_context: BoundedContext,
pub access_mapping: RoleAccessMapping,
pub created_at: chrono::DateTime<chrono::Utc>,
pub from_sequence: i64,
pub event_type: EventType,
pub filters: Vec<Filter>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Subscription {
AllEventsSubscription(Box<AllEventsSubscription>),
EventTypeSubscription(Box<EventTypeSubscription>),
}
impl Subscription {
fn bounded_context(&self) -> &BoundedContext {
match self {
Subscription::AllEventsSubscription(s) => &s.bounded_context,
Subscription::EventTypeSubscription(s) => &s.bounded_context,
}
}
fn access_mapping(&self) -> &RoleAccessMapping {
match self {
Subscription::AllEventsSubscription(s) => &s.access_mapping,
Subscription::EventTypeSubscription(s) => &s.access_mapping,
}
}
pub fn set_access_mapping(&mut self, access_mapping: RoleAccessMapping) {
match self {
Subscription::AllEventsSubscription(s) => s.access_mapping = access_mapping,
Subscription::EventTypeSubscription(s) => s.access_mapping = access_mapping,
}
}
fn starting_sequence(&self) -> i64 {
match self {
Subscription::AllEventsSubscription(s) => s.from_sequence,
Subscription::EventTypeSubscription(s) => s.from_sequence,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum AckMode {
AutoAdvance,
ManualAck,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReadCursor {
pub token: EventReadToken,
pub ack_mode: AckMode,
pub sequence: i64,
pub updated_at: chrono::DateTime<chrono::Utc>,
pub checked_out_at: Option<chrono::DateTime<chrono::Utc>>,
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[error("this bounded context is archived and accepts no new writes")]
BoundedContextArchived,
#[error("this schema change is not backwards compatible")]
SchemaIncompatible,
#[error(
"a scheduled EventType must declare both system_triggered_schedule and \
missed_occurrence_policy - neither has a default"
)]
MissingScheduleOrPolicy,
#[error(
"this filter is invalid: its field's declared type doesn't support the operator, \
its value is longer than 4096 characters (an IS_LIKE pattern, 1024), or more \
than 32 filters were given"
)]
InvalidFilter,
#[error("at most 32 tags may be given in one query")]
TooManyTags,
#[error("this schema is not well-formed JSON Schema")]
InvalidSchema,
#[error("this tag mapping names a field the schema doesn't declare")]
InvalidTagMapping,
#[error("this owner_tag_key does not name a key present in tag_mappings")]
InvalidOwnerTagKey,
#[error("a TagMapping and a SensitiveField may not name the same field")]
SensitiveFieldTagOverlap,
#[error("this sensitive field names a field the schema doesn't declare")]
InvalidSensitiveField,
#[error(
"this private field names a field the schema doesn't declare, or its team/addressee_field \
presence doesn't match its kind"
)]
InvalidPrivateField,
#[error("a private field may not also be a tag mapping or a sensitive field")]
PrivateFieldOverlap,
#[error("a re-registration may not drop a tag mapping key already in use")]
TagMappingKeyDropped,
#[error("a blind tag mapping may not be the owner_tag_key, nor belong to a scheduled type")]
InvalidBlindTagMapping,
#[error(
"a tag key is blind in one registration of this bounded context and plain in another, \
or has no BlindTag declared"
)]
BlindTagMismatch,
#[error(
"blind tag `{tag_key}` is stored with normalizer version {stored}, this process declares \
{declared}"
)]
BlindNormalizerChanged {
tag_key: String,
stored: i64,
declared: u32,
},
#[error(
"blind tag `{tag_key}` of bounded context `{bounded_context}` was hashed with a different \
blind_index_key than this process's"
)]
BlindIndexKeyChanged {
bounded_context: String,
tag_key: String,
},
#[error("tag key `{0}` is already mapped in the clear by a registered type")]
BlindTagKeyInUse(String),
#[error("this EncryptionKey is not active")]
EncryptionKeyNotActive,
#[error("this named EventType belongs to a different bounded context than the grant's")]
EventTypeNotInBoundedContext,
#[error("this named CommandType belongs to a different bounded context than the grant's")]
CommandTypeNotInBoundedContext,
#[error("this triggered_event belongs to a different bounded context than the grant's")]
TriggeredEventNotInBoundedContext,
#[error("this after_command belongs to a different bounded context than the grant's")]
AfterCommandNotInBoundedContext,
#[error("this EventReadToken's cursor mode doesn't match what was requested")]
CursorAckModeMismatch,
#[error("acknowledging requires a manual_ack cursor")]
NotManualAckCursor,
#[error("this EventType is not opted into event reads")]
EventReadNotAllowed,
#[error("this EventReadToken has no read cursor yet - consume before acknowledging")]
NoReadCursor,
#[error("an acknowledgement may not move a cursor backwards")]
AcknowledgementRegresses,
#[error("this EventType is not opted into external event ingestion")]
ExternalCreationNotAllowed,
#[error("this EventType is not opted into direct event creation")]
DirectCreationNotAllowed,
#[error("this CommandType is not opted into REST triggering")]
RestTriggerNotAllowed,
#[error("this payload does not validate against its type's registered schema")]
PayloadDoesNotMatchSchema,
#[error(
"this idempotency key uses a reserved prefix - {RESERVED_IDEMPOTENCY_KEY_PREFIX:?} \
is reserved for skilj's own internal use"
)]
ReservedIdempotencyKeyPrefix,
#[error("an idempotency key may be at most 255 characters")]
IdempotencyKeyTooLong,
#[error("an external event takes either a dedupe cursor or an idempotency key, not both")]
DedupeAndIdempotencyKey,
#[error(
"this position was read in database epoch {supplied}, but the database is now in \
epoch {current} (a failover or restore): events after an earlier point may be gone \
and their sequences reused - start again from a position read in the current epoch"
)]
EpochChanged {
supplied: String,
current: String,
},
#[error("decide() named an event type this bounded context has not registered: {0}")]
UnregisteredEventType(String),
#[error("stored payload did not decode into its expected type: {0}")]
PayloadDecodeFailed(String),
#[error("correlation_id/causation_id must be at most {CORRELATION_ID_MAX_LEN} characters")]
CorrelationIdTooLong,
#[error("parked delivery request does not match its kind's request shape: {0}")]
InvalidParkedDeliveryRequest(String),
#[error(
"this instance does not declare sync projection {0:?}, which consumes this event type"
)]
SyncProjectionNotDeclared(String),
}
impl SkiljRejection for Error {
fn code(&self) -> &str {
match self {
Error::BoundedContextArchived => "bounded_context_archived",
Error::SchemaIncompatible => "schema_incompatible",
Error::MissingScheduleOrPolicy => "missing_schedule_or_policy",
Error::InvalidFilter => "invalid_filter",
Error::TooManyTags => "too_many_tags",
Error::InvalidSchema => "invalid_schema",
Error::InvalidTagMapping => "invalid_tag_mapping",
Error::InvalidOwnerTagKey => "invalid_owner_tag_key",
Error::SensitiveFieldTagOverlap => "sensitive_field_tag_overlap",
Error::InvalidSensitiveField => "invalid_sensitive_field",
Error::InvalidPrivateField => "invalid_private_field",
Error::PrivateFieldOverlap => "private_field_overlap",
Error::TagMappingKeyDropped => "tag_mapping_key_dropped",
Error::InvalidBlindTagMapping => "invalid_blind_tag_mapping",
Error::BlindTagMismatch => "blind_tag_mismatch",
Error::BlindNormalizerChanged { .. } => "blind_normalizer_changed",
Error::BlindTagKeyInUse(_) => "blind_tag_key_in_use",
Error::BlindIndexKeyChanged { .. } => "blind_index_key_changed",
Error::EventTypeNotInBoundedContext => "event_type_not_in_bounded_context",
Error::CommandTypeNotInBoundedContext => "command_type_not_in_bounded_context",
Error::TriggeredEventNotInBoundedContext => "triggered_event_not_in_bounded_context",
Error::AfterCommandNotInBoundedContext => "after_command_not_in_bounded_context",
Error::EncryptionKeyNotActive => "encryption_key_not_active",
Error::CursorAckModeMismatch => "cursor_ack_mode_mismatch",
Error::NotManualAckCursor => "not_manual_ack_cursor",
Error::EventReadNotAllowed => "event_read_not_allowed",
Error::NoReadCursor => "no_read_cursor",
Error::AcknowledgementRegresses => "acknowledgement_regresses",
Error::ExternalCreationNotAllowed => "external_creation_not_allowed",
Error::DirectCreationNotAllowed => "direct_creation_not_allowed",
Error::RestTriggerNotAllowed => "rest_trigger_not_allowed",
Error::PayloadDoesNotMatchSchema => "payload_does_not_match_schema",
Error::ReservedIdempotencyKeyPrefix => "reserved_idempotency_key_prefix",
Error::IdempotencyKeyTooLong => "idempotency_key_too_long",
Error::DedupeAndIdempotencyKey => "dedupe_and_idempotency_key",
Error::EpochChanged { .. } => "epoch_changed",
Error::UnregisteredEventType(_) => "unregistered_event_type",
Error::PayloadDecodeFailed(_) => "payload_decode_failed",
Error::CorrelationIdTooLong => "correlation_id_too_long",
Error::InvalidParkedDeliveryRequest(_) => "invalid_parked_delivery_request",
Error::SyncProjectionNotDeclared(_) => "sync_projection_not_declared",
}
}
fn message(&self) -> String {
self.to_string()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum FieldKind {
Scalar {
json_type: &'static str,
format: Option<String>,
},
ListOfScalar,
Other,
}
fn json_type_str(type_value: &serde_json::Value) -> Option<&str> {
match type_value {
serde_json::Value::String(s) => Some(s.as_str()),
serde_json::Value::Array(arr) => {
arr.iter().find_map(|v| v.as_str().filter(|s| *s != "null"))
}
_ => None,
}
}
fn classify(
schema: &serde_json::Value,
definitions: Option<&serde_json::Map<String, serde_json::Value>>,
) -> FieldKind {
if let Some(def_name) = schema
.get("$ref")
.and_then(|v| v.as_str())
.and_then(|r| r.rsplit('/').next())
{
return definitions
.and_then(|defs| defs.get(def_name))
.map(|resolved| classify(resolved, definitions))
.unwrap_or(FieldKind::Other);
}
match schema.get("type").and_then(json_type_str) {
Some(t @ ("string" | "integer" | "number" | "boolean")) => FieldKind::Scalar {
json_type: match t {
"string" => "string",
"integer" => "integer",
"number" => "number",
_ => "boolean",
},
format: schema
.get("format")
.and_then(|f| f.as_str())
.map(String::from),
},
Some("array") => match schema
.get("items")
.and_then(|i| i.get("type"))
.and_then(json_type_str)
{
Some("string" | "integer" | "number" | "boolean") => FieldKind::ListOfScalar,
_ => FieldKind::Other, },
_ => FieldKind::Other, }
}
fn resolve_field_kind(
properties: &serde_json::Map<String, serde_json::Value>,
definitions: Option<&serde_json::Map<String, serde_json::Value>>,
field: &str,
) -> Option<FieldKind> {
match classify(resolve_field(properties, definitions, field)?, definitions) {
FieldKind::Other => None,
kind => Some(kind),
}
}
fn filter_operator_is_valid(kind: &FieldKind, operator: FilterOperator) -> bool {
match kind {
FieldKind::Scalar {
json_type: "string",
format,
} => {
matches!(
operator,
FilterOperator::Equals
| FilterOperator::Contains
| FilterOperator::IsLike
| FilterOperator::In
) || (matches!(
format.as_deref(),
Some("date-time" | "date" | "partial-date-time")
) && matches!(
operator,
FilterOperator::GreaterThan | FilterOperator::LessThan
)) || (format.as_deref() == Some("geo-point") && operator == FilterOperator::Near)
|| (format.as_deref() == Some("color") && operator == FilterOperator::SimilarColor)
|| (format.as_deref() == Some("ip") && operator == FilterOperator::InSubnet)
}
FieldKind::Scalar {
json_type: "integer" | "number",
..
} => matches!(
operator,
FilterOperator::Equals
| FilterOperator::GreaterThan
| FilterOperator::LessThan
| FilterOperator::In
),
FieldKind::Scalar {
json_type: "boolean",
..
} => matches!(operator, FilterOperator::Equals | FilterOperator::In),
FieldKind::Scalar { .. } => false, FieldKind::ListOfScalar => operator == FilterOperator::Contains,
FieldKind::Other => false,
}
}
pub fn valid_filters(event_type: &EventType, filters: &[Filter]) -> bool {
if filters.is_empty() {
return true;
}
if filters.len() > MAX_FILTERS
|| filters
.iter()
.any(|f| f.value.chars().count() > MAX_FILTER_VALUE_CHARS)
{
return false;
}
let Some(properties) = schema_properties(&event_type.schema) else {
return false;
};
let definitions = schema_definitions(&event_type.schema);
filters.iter().all(|f| {
resolve_field_kind(&properties, definitions.as_ref(), &f.field)
.is_some_and(|kind| filter_operator_is_valid(&kind, f.operator))
&& (f.operator != FilterOperator::IsLike
|| f.value.chars().count() <= MAX_LIKE_PATTERN_CHARS)
})
}
fn schema_properties(schema: &str) -> Option<serde_json::Map<String, serde_json::Value>> {
let parsed: serde_json::Value = serde_json::from_str(schema).ok()?;
parsed.get("properties")?.as_object().cloned()
}
fn schema_definitions(schema: &str) -> Option<serde_json::Map<String, serde_json::Value>> {
let parsed: serde_json::Value = serde_json::from_str(schema).ok()?;
parsed.get("definitions")?.as_object().cloned()
}
fn schema_required(schema: &str) -> Vec<String> {
serde_json::from_str::<serde_json::Value>(schema)
.ok()
.and_then(|v| v.get("required").cloned())
.and_then(|v| v.as_array().cloned())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_default()
}
fn resolve_field<'a>(
properties: &'a serde_json::Map<String, serde_json::Value>,
definitions: Option<&'a serde_json::Map<String, serde_json::Value>>,
field: &str,
) -> Option<&'a serde_json::Value> {
match field.split_once('.') {
None => properties.get(field),
Some((outer, inner)) => {
let def_name = properties
.get(outer)?
.get("$ref")?
.as_str()?
.rsplit('/')
.next()?;
definitions?.get(def_name)?.get("properties")?.get(inner)
}
}
}
pub(crate) fn payload_field_value<'a>(
payload: &'a serde_json::Value,
field: &str,
) -> Option<&'a serde_json::Value> {
match field.split_once('.') {
None => payload.get(field),
Some((outer, inner)) => payload.get(outer)?.get(inner),
}
}
pub(crate) fn payload_field_value_mut<'a>(
payload: &'a mut serde_json::Value,
field: &str,
) -> Option<&'a mut serde_json::Value> {
match field.split_once('.') {
None => payload.get_mut(field),
Some((outer, inner)) => payload.get_mut(outer)?.get_mut(inner),
}
}
pub(crate) fn json_scalar_to_string(value: &serde_json::Value) -> Option<String> {
match value {
serde_json::Value::String(s) => Some(s.clone()),
serde_json::Value::Number(_) | serde_json::Value::Bool(_) => Some(value.to_string()),
_ => None,
}
}
pub fn valid_schema(schema: &str) -> bool {
let Ok(schema_value) = serde_json::from_str::<serde_json::Value>(schema) else {
return false;
};
jsonschema::validator_for(&schema_value).is_ok()
}
pub fn valid_tag_mappings(schema: &str, tag_mappings: &[TagMapping]) -> bool {
if tag_mappings.is_empty() {
return true;
}
let Some(properties) = schema_properties(schema) else {
return false;
};
let definitions = schema_definitions(schema);
tag_mappings
.iter()
.all(|m| resolve_field_kind(&properties, definitions.as_ref(), &m.field).is_some())
}
pub fn valid_owner_tag_key(tag_mappings: &[TagMapping], owner_tag_key: Option<&str>) -> bool {
match owner_tag_key {
None => true,
Some(key) => tag_mappings.iter().any(|m| m.key == key),
}
}
pub fn valid_blind_tag_mappings(
tag_mappings: &[TagMapping],
owner_tag_key: Option<&str>,
system_triggered: bool,
) -> bool {
tag_mappings.iter().filter(|m| m.blind).all(|m| {
!system_triggered
&& owner_tag_key != Some(m.key.as_str())
&& tag_mappings.iter().all(|n| n.key != m.key || n.blind)
})
}
pub fn blind_unchanged(existing: &[TagMapping], new: &[TagMapping]) -> bool {
existing.iter().all(|m| {
new.iter()
.filter(|n| n.key == m.key)
.all(|n| n.blind == m.blind)
})
}
pub const CORRELATION_ID_MAX_LEN: usize = 200;
pub fn valid_correlation_id(id: Option<&str>) -> bool {
match id {
None => true,
Some(id) => id.is_empty() || id.chars().count() <= CORRELATION_ID_MAX_LEN,
}
}
pub fn valid_sensitive_fields(schema: &str, sensitive_fields: &[SensitiveField]) -> bool {
if sensitive_fields.is_empty() {
return true;
}
let Some(properties) = schema_properties(schema) else {
return false;
};
let definitions = schema_definitions(schema);
sensitive_fields.iter().all(|s| {
matches!(
resolve_field_kind(&properties, definitions.as_ref(), &s.field),
Some(FieldKind::Scalar {
json_type: "string",
..
})
) && resolve_field_kind(&properties, definitions.as_ref(), &s.subject_field).is_some()
})
}
pub fn valid_private_fields(schema: &str, private_fields: &[PrivateField]) -> bool {
if private_fields.is_empty() {
return true;
}
let Some(properties) = schema_properties(schema) else {
return false;
};
let definitions = schema_definitions(schema);
private_fields.iter().all(|p| {
if resolve_field_kind(&properties, definitions.as_ref(), &p.field).is_none() {
return false;
}
match p.kind {
PrivateFieldKind::Own => p.team.is_none() && p.addressee_field.is_none(),
PrivateFieldKind::Team => p.team.is_some() && p.addressee_field.is_none(),
PrivateFieldKind::Addressed => {
p.team.is_none()
&& p.addressee_field.as_deref().is_some_and(|af| {
resolve_field_kind(&properties, definitions.as_ref(), af).is_some()
})
}
}
})
}
pub fn valid_payload(schema: &str, payload: &str) -> bool {
let Ok(payload_value) = serde_json::from_str::<serde_json::Value>(payload) else {
return false;
};
let Some(validator) = compiled_schema(schema) else {
return false;
};
validator.is_valid(&payload_value)
}
const MAX_COMPILED_SCHEMAS: usize = 256;
fn compiled_schema(schema: &str) -> Option<std::sync::Arc<jsonschema::Validator>> {
static CACHE: std::sync::LazyLock<
std::sync::Mutex<std::collections::HashMap<String, std::sync::Arc<jsonschema::Validator>>>,
> = std::sync::LazyLock::new(Default::default);
if let Some(validator) = CACHE
.lock()
.expect("the compiled-schema cache is never poisoned")
.get(schema)
{
return Some(validator.clone());
}
let schema_value = serde_json::from_str::<serde_json::Value>(schema).ok()?;
let validator = std::sync::Arc::new(jsonschema::validator_for(&schema_value).ok()?);
let mut cache = CACHE
.lock()
.expect("the compiled-schema cache is never poisoned");
if cache.len() >= MAX_COMPILED_SCHEMAS {
cache.clear();
}
cache.insert(schema.to_string(), validator.clone());
Some(validator)
}
pub const RESERVED_IDEMPOTENCY_KEY_PREFIX: &str = "skilj-cross-context-route:";
pub const RESERVED_DEADLINE_IDEMPOTENCY_KEY_PREFIX: &str = "skilj-deadline:";
pub const RESERVED_PARKED_DELIVERY_IDEMPOTENCY_KEY_PREFIX: &str = "skilj-parked-delivery:";
pub const RESERVED_APPROVAL_IDEMPOTENCY_KEY_PREFIX: &str = "skilj-approval:";
pub const MAX_IDEMPOTENCY_KEY_CHARS: usize = 255;
const _: () = assert!(MAX_IDEMPOTENCY_KEY_CHARS == 255);
pub fn reject_reserved_idempotency_key(idempotency_key: Option<&str>) -> crate::error::Result<()> {
if idempotency_key.is_some_and(|key| key.chars().count() > MAX_IDEMPOTENCY_KEY_CHARS) {
return Err(Error::IdempotencyKeyTooLong.into());
}
match idempotency_key {
Some(key)
if key.starts_with(RESERVED_IDEMPOTENCY_KEY_PREFIX)
|| key.starts_with(RESERVED_DEADLINE_IDEMPOTENCY_KEY_PREFIX)
|| key.starts_with(RESERVED_PARKED_DELIVERY_IDEMPOTENCY_KEY_PREFIX)
|| key.starts_with(RESERVED_APPROVAL_IDEMPOTENCY_KEY_PREFIX) =>
{
Err(Error::ReservedIdempotencyKeyPrefix.into())
}
_ => Ok(()),
}
}
pub fn schema_is_backwards_compatible(existing_schema: &str, schema: &str) -> bool {
if existing_schema == schema {
return true;
}
let (Some(existing_properties), Some(new_properties)) = (
schema_properties(existing_schema),
schema_properties(schema),
) else {
return false;
};
let existing_required = schema_required(existing_schema);
let new_required = schema_required(schema);
for (field, existing_def) in &existing_properties {
let Some(new_def) = new_properties.get(field) else {
return false; };
if existing_def.get("type") != new_def.get("type") {
return false; }
let was_required = existing_required.iter().any(|f| f == field);
let now_required = new_required.iter().any(|f| f == field);
if !was_required && now_required {
return false; }
}
for field in new_properties.keys() {
if !existing_properties.contains_key(field) && new_required.iter().any(|f| f == field) {
return false; }
}
true
}
pub const MAX_LIKE_PATTERN_CHARS: usize = 1024;
pub const MAX_FILTERS: usize = 32;
pub const MAX_FILTER_VALUE_CHARS: usize = 4096;
pub const MAX_QUERY_TAGS: usize = 32;
const _: () = assert!(
MAX_LIKE_PATTERN_CHARS == 1024
&& MAX_FILTERS == 32
&& MAX_FILTER_VALUE_CHARS == 4096
&& MAX_QUERY_TAGS == 32
);
pub fn valid_query_tags(tags: Option<&[Tag]>) -> crate::error::Result<()> {
if tags.is_some_and(|tags| tags.len() > MAX_QUERY_TAGS) {
return Err(Error::TooManyTags.into());
}
Ok(())
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct QueryTag {
pub tag: Tag,
pub plaintext: bool,
}
pub fn resolve_query_tags(
tags: Vec<QueryTag>,
blind_keys: &[String],
blind: &BlindIndex,
bounded_context: &str,
access_mapping: &RoleAccessMapping,
) -> crate::error::Result<Vec<Tag>> {
tags.into_iter()
.map(|QueryTag { tag, plaintext }| {
let hashed = plaintext && blind_keys.contains(&tag.key);
match tag.value {
Some(value) if hashed => {
if !access_mapping.can_read_sensitive {
return Err(
crate::blind::Error::LookupNotGranted { tag_key: tag.key }.into()
);
}
let value = blind.blind(bounded_context, &tag.key, &value)?;
Ok(Tag {
key: tag.key,
value: Some(value),
})
}
value => Ok(Tag {
key: tag.key,
value,
}),
}
})
.collect()
}
fn like_matches(text: &str, pattern: &str) -> bool {
let mut p: Vec<char> = Vec::new();
for c in pattern.chars() {
if !(c == '%' && p.last() == Some(&'%')) {
p.push(c);
}
}
let words = (p.len() + 1).div_ceil(64);
let set = |bits: &mut [u64], j: usize| bits[j / 64] |= 1 << (j % 64);
let mut percent = vec![0u64; words];
let mut any_char = vec![0u64; words];
let mut literal: std::collections::HashMap<char, Vec<u64>> = std::collections::HashMap::new();
for (i, &c) in p.iter().enumerate() {
match c {
'%' => set(&mut percent, i + 1),
'_' => set(&mut any_char, i + 1),
c => set(literal.entry(c).or_insert_with(|| vec![0; words]), i + 1),
}
}
let mut row = vec![0u64; words];
set(&mut row, 0);
if p.first() == Some(&'%') {
set(&mut row, 1);
}
let mut next = vec![0u64; words];
for c in text.chars() {
let literal = literal.get(&c);
let mut carry = 0;
for w in 0..words {
let shifted = (row[w] << 1) | carry;
carry = row[w] >> 63;
let matches = any_char[w] | literal.map_or(0, |l| l[w]);
next[w] = (shifted & matches) | (row[w] & percent[w]);
}
let mut carry = 0;
let mut any = 0;
for w in 0..words {
let shifted = (next[w] << 1) | carry;
carry = next[w] >> 63;
next[w] |= shifted & percent[w];
any |= next[w];
}
if any == 0 {
return false;
}
std::mem::swap(&mut row, &mut next);
}
row[p.len() / 64] >> (p.len() % 64) & 1 == 1
}
fn string_ordering(payload: &str, filter_value: &str, operator: FilterOperator) -> bool {
fn cmp<T: PartialOrd>(a: T, b: T, operator: FilterOperator) -> bool {
match operator {
FilterOperator::GreaterThan => a > b,
FilterOperator::LessThan => a < b,
_ => false,
}
}
if let (Ok(a), Ok(b)) = (
chrono::DateTime::parse_from_rfc3339(payload),
chrono::DateTime::parse_from_rfc3339(filter_value),
) {
return cmp(a, b, operator);
}
if let (Ok(a), Ok(b)) = (
chrono::NaiveDate::parse_from_str(payload, "%Y-%m-%d"),
chrono::NaiveDate::parse_from_str(filter_value, "%Y-%m-%d"),
) {
return cmp(a, b, operator);
}
if let (Ok(a), Ok(b)) = (
chrono::NaiveTime::parse_from_str(payload, "%H:%M:%S%.f"),
chrono::NaiveTime::parse_from_str(filter_value, "%H:%M:%S%.f"),
) {
return cmp(a, b, operator);
}
false
}
fn geo_distance_within(payload: &str, filter_value: &str) -> bool {
fn parse_point(s: &str) -> Option<(f64, f64)> {
let mut parts = s.splitn(2, ',');
let lat = parts.next()?.trim().parse::<f64>().ok()?;
let lng = parts.next()?.trim().parse::<f64>().ok()?;
Some((lat, lng))
}
let Some((lat1, lng1)) = parse_point(payload) else {
return false;
};
let mut filter_parts = filter_value.splitn(3, ',');
let (Some(lat2_str), Some(lng2_str), Some(radius_str)) = (
filter_parts.next(),
filter_parts.next(),
filter_parts.next(),
) else {
return false;
};
let (Ok(lat2), Ok(lng2), Ok(radius_meters)) = (
lat2_str.trim().parse::<f64>(),
lng2_str.trim().parse::<f64>(),
radius_str.trim().parse::<f64>(),
) else {
return false;
};
const EARTH_RADIUS_METERS: f64 = 6_371_000.0;
let (phi1, phi2) = (lat1.to_radians(), lat2.to_radians());
let d_phi = (lat2 - lat1).to_radians();
let d_lambda = (lng2 - lng1).to_radians();
let a = (d_phi / 2.0).sin().powi(2) + phi1.cos() * phi2.cos() * (d_lambda / 2.0).sin().powi(2);
let distance_meters = EARTH_RADIUS_METERS * 2.0 * a.sqrt().atan2((1.0 - a).sqrt());
distance_meters <= radius_meters
}
fn color_similarity_within(payload: &str, filter_value: &str) -> bool {
fn parse_hex(s: &str) -> Option<(f64, f64, f64)> {
let s = s.trim().strip_prefix('#')?;
if s.len() != 6 {
return None;
}
let r = u8::from_str_radix(&s[0..2], 16).ok()? as f64;
let g = u8::from_str_radix(&s[2..4], 16).ok()? as f64;
let b = u8::from_str_radix(&s[4..6], 16).ok()? as f64;
Some((r, g, b))
}
let Some((r1, g1, b1)) = parse_hex(payload) else {
return false;
};
let mut filter_parts = filter_value.splitn(2, ',');
let (Some(color_str), Some(max_distance_str)) = (filter_parts.next(), filter_parts.next())
else {
return false;
};
let Some((r2, g2, b2)) = parse_hex(color_str) else {
return false;
};
let Ok(max_distance) = max_distance_str.trim().parse::<f64>() else {
return false;
};
let distance = ((r1 - r2).powi(2) + (g1 - g2).powi(2) + (b1 - b2).powi(2)).sqrt();
distance <= max_distance
}
fn ip_in_subnet(payload: &str, filter_value: &str) -> bool {
let Ok(addr) = payload.trim().parse::<std::net::IpAddr>() else {
return false;
};
let Some((network, prefix)) = parse_cidr(filter_value.trim()) else {
return false;
};
match (addr, network) {
(std::net::IpAddr::V4(addr), std::net::IpAddr::V4(network)) => {
mask_v4(u32::from(addr), prefix) == mask_v4(u32::from(network), prefix)
}
(std::net::IpAddr::V6(addr), std::net::IpAddr::V6(network)) => {
mask_v6(u128::from(addr), prefix) == mask_v6(u128::from(network), prefix)
}
_ => false,
}
}
fn parse_cidr(raw: &str) -> Option<(std::net::IpAddr, u8)> {
let (addr, prefix) = raw.split_once('/')?;
let addr: std::net::IpAddr = addr.trim().parse().ok()?;
let prefix: u8 = prefix.trim().parse().ok()?;
let max = if addr.is_ipv4() { 32 } else { 128 };
(prefix <= max).then_some((addr, prefix))
}
fn mask_v4(value: u32, prefix: u8) -> u32 {
if prefix == 0 {
0
} else {
value & (u32::MAX << (32 - prefix))
}
}
fn mask_v6(value: u128, prefix: u8) -> u128 {
if prefix == 0 {
0
} else {
value & (u128::MAX << (128 - prefix))
}
}
fn matches_any_of(value: &serde_json::Value, filter_value: &str) -> bool {
let Some(value_as_string) = json_scalar_to_string(value) else {
return false;
};
filter_value
.split(',')
.any(|candidate| candidate == value_as_string)
}
fn matches_one_filter(payload: &serde_json::Value, filter: &Filter) -> bool {
let Some(value) = payload_field_value(payload, &filter.field) else {
return false;
};
match value {
serde_json::Value::Array(items) => {
filter.operator == FilterOperator::Contains
&& items.iter().any(|item| {
json_scalar_to_string(item).as_deref() == Some(filter.value.as_str())
})
}
serde_json::Value::String(s) => match filter.operator {
FilterOperator::Equals => s == &filter.value,
FilterOperator::Contains => s.contains(filter.value.as_str()),
FilterOperator::IsLike => like_matches(s, &filter.value),
FilterOperator::GreaterThan | FilterOperator::LessThan => {
string_ordering(s, &filter.value, filter.operator)
}
FilterOperator::Near => geo_distance_within(s, &filter.value),
FilterOperator::SimilarColor => color_similarity_within(s, &filter.value),
FilterOperator::InSubnet => ip_in_subnet(s, &filter.value),
FilterOperator::In => matches_any_of(value, &filter.value),
},
serde_json::Value::Number(n) => {
if filter.operator == FilterOperator::In {
return matches_any_of(value, &filter.value);
}
let (Some(a), Ok(b)) = (n.as_f64(), filter.value.parse::<f64>()) else {
return false;
};
match filter.operator {
FilterOperator::Equals => a == b,
FilterOperator::GreaterThan => a > b,
FilterOperator::LessThan => a < b,
FilterOperator::Contains
| FilterOperator::IsLike
| FilterOperator::Near
| FilterOperator::SimilarColor
| FilterOperator::InSubnet
| FilterOperator::In => false,
}
}
serde_json::Value::Bool(b) => match filter.operator {
FilterOperator::Equals => b.to_string() == filter.value,
FilterOperator::In => matches_any_of(value, &filter.value),
_ => false,
},
serde_json::Value::Null | serde_json::Value::Object(_) => false,
}
}
pub fn matches_filters(event: &Event, filters: &[Filter]) -> bool {
if filters.is_empty() {
return true;
}
let Ok(payload) = serde_json::from_str::<serde_json::Value>(&event.payload) else {
return false;
};
filters.iter().all(|f| matches_one_filter(&payload, f))
}
pub fn tag_owner_scope_satisfied(
tags: &[Tag],
owner_tag_key: Option<&str>,
scope: Option<&str>,
) -> bool {
let Some(scope) = scope else {
return true;
};
let Some(owner_tag_key) = owner_tag_key else {
return true;
};
tags.iter()
.any(|tag| tag.key == owner_tag_key && tag.value.as_deref() == Some(scope))
}
pub fn event_owner_scope_satisfied(event: &Event, scope: Option<&str>) -> bool {
tag_owner_scope_satisfied(
&event.tags,
event.event_type.owner_tag_key.as_deref(),
scope,
)
}
pub fn command_owner_scope_satisfied(command: &Command, scope: Option<&str>) -> bool {
tag_owner_scope_satisfied(
&command.consistency_tags,
command.command_type.owner_tag_key.as_deref(),
scope,
)
}
fn highest_sequence(events: &[Event]) -> Option<i64> {
events.iter().map(|e| e.sequence).max()
}
pub fn protect_sensitive_fields(
sensitive_fields: &[SensitiveField],
payload: &str,
resolve_key: impl Fn(&str, &str) -> (EncryptionKey, DataKey),
) -> ProtectedPayload {
if sensitive_fields.is_empty() {
return ProtectedPayload {
payload: payload.to_string(),
encryption_keys: Vec::new(),
};
}
let mut parsed: serde_json::Value = serde_json::from_str(payload).expect(
"protect_sensitive_fields: payload is always valid JSON by the time this is called",
);
let mut used_keys: Vec<EncryptionKey> = Vec::new();
for sf in sensitive_fields {
let Some(subject_value) =
payload_field_value(&parsed, &sf.subject_field).and_then(json_scalar_to_string)
else {
continue;
};
let Some(plaintext) = payload_field_value(&parsed, &sf.field).map(|v| {
v.as_str()
.map(str::to_string)
.unwrap_or_else(|| v.to_string())
}) else {
continue;
};
let (key, data_key) = resolve_key(&sf.subject_key, &subject_value);
let ciphertext = crate::encryption::encrypt_leaf(&data_key, &plaintext);
if let Some(slot) = payload_field_value_mut(&mut parsed, &sf.field) {
*slot = serde_json::Value::String(ciphertext);
}
if !used_keys.contains(&key) {
used_keys.push(key);
}
}
ProtectedPayload {
payload: serde_json::to_string(&parsed)
.expect("re-serialising a parsed JSON Value is infallible"),
encryption_keys: used_keys,
}
}
pub fn reveal_sensitive_fields(
sensitive_fields: &[SensitiveField],
payload: &str,
resolve_key: impl Fn(&str, &str) -> Option<DataKey>,
) -> crate::error::Result<String> {
if sensitive_fields.is_empty() {
return Ok(payload.to_string());
}
let mut parsed: serde_json::Value = serde_json::from_str(payload)
.expect("reveal_sensitive_fields: payload is always valid JSON by the time this is called");
for sf in sensitive_fields {
let Some(subject_value) =
payload_field_value(&parsed, &sf.subject_field).and_then(json_scalar_to_string)
else {
continue;
};
let Some(ciphertext) = payload_field_value(&parsed, &sf.field)
.and_then(|v| v.as_str())
.map(str::to_string)
else {
continue;
};
let key =
resolve_key(&sf.subject_key, &subject_value).ok_or(Error::EncryptionKeyNotActive)?;
let plaintext = crate::encryption::decrypt_leaf(&key, &ciphertext)?;
if let Some(slot) = payload_field_value_mut(&mut parsed, &sf.field) {
*slot = serde_json::Value::String(plaintext);
}
}
Ok(serde_json::to_string(&parsed).expect("re-serialising a parsed JSON Value is infallible"))
}
pub fn sensitive_field_subjects(
sensitive_fields: &[SensitiveField],
payload: &str,
) -> Vec<(String, String)> {
if sensitive_fields.is_empty() {
return Vec::new();
}
let parsed: serde_json::Value = serde_json::from_str(payload).expect(
"sensitive_field_subjects: payload is always valid JSON by the time this is called",
);
let mut subjects = Vec::new();
for sf in sensitive_fields {
if let Some(subject_value) =
payload_field_value(&parsed, &sf.subject_field).and_then(json_scalar_to_string)
{
let pair = (sf.subject_key.clone(), subject_value);
if !subjects.contains(&pair) {
subjects.push(pair);
}
}
}
subjects
}
fn push_unique_tag(tags: &mut Vec<Tag>, key: &str, value: Option<String>) {
let tag = Tag {
key: key.to_string(),
value,
};
if !tags.contains(&tag) {
tags.push(tag);
}
}
pub fn derive_tags(
tag_mappings: &[TagMapping],
payload: &str,
blind: &BlindIndex,
bounded_context: &str,
) -> crate::error::Result<Vec<Tag>> {
derive_mapped_tags(tag_mappings, payload, blind, bounded_context)
}
pub fn derive_plain_tags(tag_mappings: &[TagMapping], payload: &str) -> Vec<Tag> {
derive_mapped_tags(
tag_mappings.iter().filter(|m| !m.blind),
payload,
&BlindIndex::default(),
"",
)
.expect("deriving plain tag mappings never hashes, so never fails")
}
fn derive_mapped_tags<'a>(
tag_mappings: impl IntoIterator<Item = &'a TagMapping>,
payload: &str,
blind: &BlindIndex,
bounded_context: &str,
) -> crate::error::Result<Vec<Tag>> {
let mut tag_mappings = tag_mappings.into_iter().peekable();
if tag_mappings.peek().is_none() {
return Ok(Vec::new());
}
let parsed: serde_json::Value =
serde_json::from_str(payload).unwrap_or(serde_json::Value::Null);
let mut tags = Vec::new();
for mapping in tag_mappings {
let blinded = mapping.blind || blind.declares(bounded_context, &mapping.key);
let tag_value = |scalar: Option<String>| -> crate::error::Result<Option<String>> {
match scalar {
Some(value) if blinded => {
Ok(Some(blind.blind(bounded_context, &mapping.key, &value)?))
}
other => Ok(other),
}
};
let value = payload_field_value(&parsed, &mapping.field);
match value.and_then(|v| v.as_array()) {
Some(elements) if !elements.is_empty() => {
for element in elements {
if let Some(scalar) = json_scalar_to_string(element) {
push_unique_tag(&mut tags, &mapping.key, tag_value(Some(scalar))?);
}
}
}
_ => {
let scalar = value.and_then(json_scalar_to_string);
push_unique_tag(&mut tags, &mapping.key, tag_value(scalar)?);
}
}
}
Ok(tags)
}
pub fn sensitive_field_is_granted(access_mapping: &RoleAccessMapping, subject_value: &str) -> bool {
access_mapping.can_read_sensitive || access_mapping.role.external_subject == subject_value
}
pub trait PrivateFieldRecord {
fn private_fields(&self) -> &[PrivateField];
fn client_id(&self) -> &str;
fn payload(&self) -> &str;
}
impl PrivateFieldRecord for Event {
fn private_fields(&self) -> &[PrivateField] {
&self.event_type.private_fields
}
fn client_id(&self) -> &str {
&self.metadata.client_id
}
fn payload(&self) -> &str {
&self.payload
}
}
impl PrivateFieldRecord for Command {
fn private_fields(&self) -> &[PrivateField] {
&self.command_type.private_fields
}
fn client_id(&self) -> &str {
&self.metadata.client_id
}
fn payload(&self) -> &str {
&self.payload
}
}
pub fn is_default_private_reader<R: PrivateFieldRecord>(record: &R, role: &Role) -> bool {
let private_fields = record.private_fields();
if private_fields
.iter()
.any(|p| p.kind == PrivateFieldKind::Own)
&& record.client_id() == role.id
{
return true;
}
if !private_fields
.iter()
.any(|p| p.kind == PrivateFieldKind::Addressed)
{
return false;
}
let Ok(parsed) = serde_json::from_str::<serde_json::Value>(record.payload()) else {
return false;
};
private_fields.iter().any(|p| {
p.kind == PrivateFieldKind::Addressed
&& p.addressee_field
.as_deref()
.and_then(|af| payload_field_value(&parsed, af))
.and_then(json_scalar_to_string)
.is_some_and(|addressee| addressee == role.external_subject)
})
}
fn entitled_to_read_private_field<R: PrivateFieldRecord>(
record: &R,
kind: PrivateFieldKind,
team: Option<&str>,
access_mapping: &RoleAccessMapping,
grants: &[PrivateFieldGrant],
matches_this_record: impl Fn(&PrivateFieldGrant) -> bool,
) -> bool {
match kind {
PrivateFieldKind::Team => {
crate::access_control::role_matches_required_team(&access_mapping.role, team)
}
PrivateFieldKind::Own | PrivateFieldKind::Addressed => {
is_default_private_reader(record, &access_mapping.role)
|| grants.iter().any(|g| {
g.status == TokenStatus::Active
&& g.grantee == access_mapping.role
&& (matches_this_record(g)
|| (g.event_sequence.is_none()
&& g.command_id.is_none()
&& is_default_private_reader(record, &g.grantor)))
})
}
}
}
pub fn render_event(
event: &Event,
access_mapping: &RoleAccessMapping,
resolve_data_key: &impl Fn(&str, &str) -> Option<DataKey>,
grants: &[PrivateFieldGrant],
) -> String {
let decrypted = decrypt_sensitive_fields(
&event.event_type.sensitive_fields,
&event.payload,
access_mapping,
resolve_data_key,
);
redact_unentitled_private_fields(event, &decrypted, access_mapping, grants, |g| {
g.bounded_context.same_as(&event.bounded_context)
&& g.event_sequence == Some(event.sequence)
})
}
pub fn render_command(
command: &Command,
access_mapping: &RoleAccessMapping,
resolve_data_key: &impl Fn(&str, &str) -> Option<DataKey>,
grants: &[PrivateFieldGrant],
) -> String {
let decrypted = decrypt_sensitive_fields(
&command.command_type.sensitive_fields,
&command.payload,
access_mapping,
resolve_data_key,
);
redact_unentitled_private_fields(command, &decrypted, access_mapping, grants, |g| {
g.bounded_context.same_as(&command.bounded_context)
&& g.command_id.as_deref() == Some(command.id.as_str())
})
}
#[derive(Debug, Clone, PartialEq)]
pub struct WouldBeEvent {
pub event_type: EventType,
pub payload: String,
pub tags: Vec<Tag>,
pub client_id: String,
}
impl PrivateFieldRecord for WouldBeEvent {
fn private_fields(&self) -> &[PrivateField] {
&self.event_type.private_fields
}
fn client_id(&self) -> &str {
&self.client_id
}
fn payload(&self) -> &str {
&self.payload
}
}
pub fn render_would_be_event(
event: &WouldBeEvent,
access_mapping: &RoleAccessMapping,
grants: &[PrivateFieldGrant],
) -> String {
let sensitive_fields = &event.event_type.sensitive_fields;
let payload = if sensitive_fields.is_empty() {
event.payload.clone()
} else {
let mut parsed: serde_json::Value = serde_json::from_str(&event.payload)
.expect("render_would_be_event: a decided event's payload is serialized JSON");
for sf in sensitive_fields {
let granted = payload_field_value(&parsed, &sf.subject_field)
.and_then(json_scalar_to_string)
.is_some_and(|subject| sensitive_field_is_granted(access_mapping, &subject));
if granted {
continue;
}
if let Some(slot) = payload_field_value_mut(&mut parsed, &sf.field) {
*slot = serde_json::Value::Null;
}
}
serde_json::to_string(&parsed).expect("re-serialising a parsed JSON Value is infallible")
};
let record = WouldBeEvent {
payload,
..event.clone()
};
redact_unentitled_private_fields(&record, &record.payload, access_mapping, grants, |_| false)
}
fn redact_unentitled_private_fields<R: PrivateFieldRecord>(
record: &R,
payload: &str,
access_mapping: &RoleAccessMapping,
grants: &[PrivateFieldGrant],
matches_this_record: impl Fn(&PrivateFieldGrant) -> bool,
) -> String {
let private_fields = record.private_fields();
if private_fields.is_empty() {
return payload.to_string();
}
let mut parsed: serde_json::Value = serde_json::from_str(payload).expect(
"redact_unentitled_private_fields: payload is always valid JSON by the time this is called",
);
for p in private_fields {
if entitled_to_read_private_field(
record,
p.kind,
p.team.as_deref(),
access_mapping,
grants,
&matches_this_record,
) {
continue;
}
if let Some(slot) = payload_field_value_mut(&mut parsed, &p.field) {
*slot = serde_json::Value::Null;
}
}
serde_json::to_string(&parsed).expect("re-serialising a parsed JSON Value is infallible")
}
pub fn redact_private_fields(event: &Event) -> Event {
let private_fields = &event.event_type.private_fields;
if private_fields.is_empty() {
return event.clone();
}
let mut parsed: serde_json::Value = serde_json::from_str(&event.payload)
.expect("redact_private_fields: payload is always valid JSON by the time this is called");
for p in private_fields {
if let Some(slot) = payload_field_value_mut(&mut parsed, &p.field) {
*slot = serde_json::Value::Null;
}
}
Event {
payload: serde_json::to_string(&parsed)
.expect("re-serialising a parsed JSON Value is infallible"),
..event.clone()
}
}
fn decrypt_sensitive_fields(
sensitive_fields: &[SensitiveField],
payload: &str,
access_mapping: &RoleAccessMapping,
resolve_data_key: &impl Fn(&str, &str) -> Option<DataKey>,
) -> String {
if sensitive_fields.is_empty() {
return payload.to_string();
}
let mut parsed: serde_json::Value = serde_json::from_str(payload).expect(
"decrypt_sensitive_fields: payload is always valid JSON by the time this is called",
);
for sf in sensitive_fields {
let Some(subject_value) =
payload_field_value(&parsed, &sf.subject_field).and_then(json_scalar_to_string)
else {
continue;
};
if !sensitive_field_is_granted(access_mapping, &subject_value) {
continue;
}
let Some(data_key) = resolve_data_key(&sf.subject_key, &subject_value) else {
continue;
};
let Some(ciphertext) = payload_field_value(&parsed, &sf.field).and_then(|v| v.as_str())
else {
continue;
};
let Ok(plaintext) = crate::encryption::decrypt_leaf(&data_key, ciphertext) else {
continue;
};
if let Some(slot) = payload_field_value_mut(&mut parsed, &sf.field) {
*slot = serde_json::Value::String(plaintext);
}
}
serde_json::to_string(&parsed).expect("re-serialising a parsed JSON Value is infallible")
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum EventTypeRegistration {
Created(EventType),
Updated(EventType),
}
impl EventTypeRegistration {
pub fn event_type(&self) -> &EventType {
match self {
EventTypeRegistration::Created(et) | EventTypeRegistration::Updated(et) => et,
}
}
}
#[allow(clippy::too_many_arguments)]
pub fn register_event_type(
access_mapping: &RoleAccessMapping,
bounded_context: &BoundedContext,
name: String,
schema: String,
tag_mappings: Vec<TagMapping>,
owner_tag_key: Option<String>,
sensitive_fields: Vec<SensitiveField>,
private_fields: Vec<PrivateField>,
external_creation_allowed: bool,
direct_creation_allowed: bool,
system_triggered_allowed: bool,
system_triggered_schedule: Option<String>,
missed_occurrence_policy: Option<MissedOccurrencePolicy>,
event_read_allowed: bool,
existing: Option<&EventType>,
now: chrono::DateTime<chrono::Utc>,
) -> crate::error::Result<EventTypeRegistration> {
if access_mapping.status != RoleStatus::Active {
return Err(crate::access_control::Error::GrantNotActive.into());
}
if access_mapping.level != AccessLevel::Admin {
return Err(crate::access_control::Error::InsufficientAccessLevel.into());
}
if !access_mapping.bounded_context.same_as(bounded_context) {
return Err(crate::access_control::Error::GrantBoundedContextMismatch.into());
}
if bounded_context.status != BoundedContextStatus::Active {
return Err(Error::BoundedContextArchived.into());
}
if !valid_schema(&schema) {
return Err(Error::InvalidSchema.into());
}
if !valid_tag_mappings(&schema, &tag_mappings) {
return Err(Error::InvalidTagMapping.into());
}
if !valid_owner_tag_key(&tag_mappings, owner_tag_key.as_deref()) {
return Err(Error::InvalidOwnerTagKey.into());
}
if !valid_sensitive_fields(&schema, &sensitive_fields) {
return Err(Error::InvalidSensitiveField.into());
}
if sensitive_fields
.iter()
.any(|s| crate::secrets::is_reserved_subject_key(&s.subject_key))
{
return Err(crate::secrets::Error::ReservedSubjectKey.into());
}
if !valid_private_fields(&schema, &private_fields) {
return Err(Error::InvalidPrivateField.into());
}
if tag_mappings
.iter()
.any(|m| !m.blind && sensitive_fields.iter().any(|s| s.field == m.field))
{
return Err(Error::SensitiveFieldTagOverlap.into());
}
if tag_mappings
.iter()
.any(|m| !m.blind && private_fields.iter().any(|p| p.field == m.field))
|| sensitive_fields
.iter()
.any(|s| private_fields.iter().any(|p| p.field == s.field))
{
return Err(Error::PrivateFieldOverlap.into());
}
if system_triggered_allowed
&& (system_triggered_schedule.is_none() || missed_occurrence_policy.is_none())
{
return Err(Error::MissingScheduleOrPolicy.into());
}
if !valid_blind_tag_mappings(
&tag_mappings,
owner_tag_key.as_deref(),
system_triggered_allowed,
) {
return Err(Error::InvalidBlindTagMapping.into());
}
let scheduling_newly_enabled =
system_triggered_allowed && existing.is_none_or(|e| !e.system_triggered_allowed);
let Some(existing) = existing else {
return Ok(EventTypeRegistration::Created(EventType {
bounded_context: bounded_context.clone(),
name,
schema,
schema_version: 1,
tag_mappings,
owner_tag_key,
sensitive_fields,
private_fields,
external_creation_allowed,
direct_creation_allowed,
system_triggered_allowed,
system_triggered_schedule,
missed_occurrence_policy,
schedule_position: scheduling_newly_enabled.then_some(now),
last_fired_at: None,
event_read_allowed,
}));
};
if !schema_is_backwards_compatible(&existing.schema, &schema) {
return Err(Error::SchemaIncompatible.into());
}
if !existing
.tag_mappings
.iter()
.all(|m| tag_mappings.iter().any(|n| n.key == m.key))
{
return Err(Error::TagMappingKeyDropped.into());
}
if !blind_unchanged(&existing.tag_mappings, &tag_mappings) {
return Err(Error::BlindTagMismatch.into());
}
let schema_changed = existing.schema != schema;
Ok(EventTypeRegistration::Updated(EventType {
bounded_context: bounded_context.clone(),
name,
schema,
schema_version: if schema_changed {
existing.schema_version + 1
} else {
existing.schema_version
},
tag_mappings,
owner_tag_key,
sensitive_fields,
private_fields,
external_creation_allowed,
direct_creation_allowed,
system_triggered_allowed,
system_triggered_schedule,
missed_occurrence_policy,
schedule_position: if scheduling_newly_enabled {
Some(now)
} else {
existing.schedule_position
},
last_fired_at: existing.last_fired_at,
event_read_allowed,
}))
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum CommandTypeRegistration {
Created(CommandType),
Updated(CommandType),
}
impl CommandTypeRegistration {
pub fn command_type(&self) -> &CommandType {
match self {
CommandTypeRegistration::Created(ct) | CommandTypeRegistration::Updated(ct) => ct,
}
}
}
#[allow(clippy::too_many_arguments)]
pub fn register_command_type(
access_mapping: &RoleAccessMapping,
bounded_context: &BoundedContext,
name: String,
schema: String,
tag_mappings: Vec<TagMapping>,
owner_tag_key: Option<String>,
sensitive_fields: Vec<SensitiveField>,
private_fields: Vec<PrivateField>,
rest_trigger_allowed: bool,
existing: Option<&CommandType>,
) -> crate::error::Result<CommandTypeRegistration> {
if access_mapping.status != RoleStatus::Active {
return Err(crate::access_control::Error::GrantNotActive.into());
}
if access_mapping.level != AccessLevel::Admin {
return Err(crate::access_control::Error::InsufficientAccessLevel.into());
}
if !access_mapping.bounded_context.same_as(bounded_context) {
return Err(crate::access_control::Error::GrantBoundedContextMismatch.into());
}
if bounded_context.status != BoundedContextStatus::Active {
return Err(Error::BoundedContextArchived.into());
}
if !valid_schema(&schema) {
return Err(Error::InvalidSchema.into());
}
if !valid_tag_mappings(&schema, &tag_mappings) {
return Err(Error::InvalidTagMapping.into());
}
if !valid_owner_tag_key(&tag_mappings, owner_tag_key.as_deref()) {
return Err(Error::InvalidOwnerTagKey.into());
}
if !valid_sensitive_fields(&schema, &sensitive_fields) {
return Err(Error::InvalidSensitiveField.into());
}
if sensitive_fields
.iter()
.any(|s| crate::secrets::is_reserved_subject_key(&s.subject_key))
{
return Err(crate::secrets::Error::ReservedSubjectKey.into());
}
if !valid_private_fields(&schema, &private_fields) {
return Err(Error::InvalidPrivateField.into());
}
if tag_mappings
.iter()
.any(|m| !m.blind && sensitive_fields.iter().any(|s| s.field == m.field))
{
return Err(Error::SensitiveFieldTagOverlap.into());
}
if tag_mappings
.iter()
.any(|m| !m.blind && private_fields.iter().any(|p| p.field == m.field))
|| sensitive_fields
.iter()
.any(|s| private_fields.iter().any(|p| p.field == s.field))
{
return Err(Error::PrivateFieldOverlap.into());
}
if !valid_blind_tag_mappings(&tag_mappings, owner_tag_key.as_deref(), false) {
return Err(Error::InvalidBlindTagMapping.into());
}
let Some(existing) = existing else {
return Ok(CommandTypeRegistration::Created(CommandType {
bounded_context: bounded_context.clone(),
name,
schema,
schema_version: 1,
tag_mappings,
owner_tag_key,
sensitive_fields,
private_fields,
rest_trigger_allowed,
}));
};
if !schema_is_backwards_compatible(&existing.schema, &schema) {
return Err(Error::SchemaIncompatible.into());
}
if !existing
.tag_mappings
.iter()
.all(|m| tag_mappings.iter().any(|n| n.key == m.key))
{
return Err(Error::TagMappingKeyDropped.into());
}
if !blind_unchanged(&existing.tag_mappings, &tag_mappings) {
return Err(Error::BlindTagMismatch.into());
}
let schema_changed = existing.schema != schema;
Ok(CommandTypeRegistration::Updated(CommandType {
bounded_context: bounded_context.clone(),
name,
schema,
schema_version: if schema_changed {
existing.schema_version + 1
} else {
existing.schema_version
},
tag_mappings,
owner_tag_key,
sensitive_fields,
private_fields,
rest_trigger_allowed,
}))
}
pub fn forget_subject(
access_mapping: &RoleAccessMapping,
key: &EncryptionKey,
now: chrono::DateTime<chrono::Utc>,
) -> crate::error::Result<EncryptionKey> {
if access_mapping.status != RoleStatus::Active {
return Err(crate::access_control::Error::GrantNotActive.into());
}
if access_mapping.level != AccessLevel::Admin {
return Err(crate::access_control::Error::InsufficientAccessLevel.into());
}
if !access_mapping.bounded_context.same_as(&key.bounded_context) {
return Err(crate::access_control::Error::GrantBoundedContextMismatch.into());
}
if key.status != EncryptionKeyStatus::Active {
return Err(Error::EncryptionKeyNotActive.into());
}
if crate::secrets::is_reserved_subject_key(&key.subject_key) {
return Err(crate::secrets::Error::ReservedSubjectKey.into());
}
Ok(EncryptionKey {
status: EncryptionKeyStatus::Destroyed,
destroyed_at: Some(now),
..key.clone()
})
}
pub const DEFAULT_MAX_EVENTS_PER_READ: usize = 1000;
#[allow(clippy::too_many_arguments)]
pub fn query_events(
access_mapping: &RoleAccessMapping,
event_types: &[EventType],
tags: Option<&[Tag]>,
after_sequence: Option<i64>,
correlation_id: Option<&str>,
bounded_context_events: &[Event],
resolve_data_key: impl Fn(&str, &str) -> Option<DataKey>,
private_field_grants: &[PrivateFieldGrant],
) -> crate::error::Result<Vec<(i64, String)>> {
query_events_page(
access_mapping,
event_types,
tags,
after_sequence,
correlation_id,
bounded_context_events,
resolve_data_key,
private_field_grants,
DEFAULT_MAX_EVENTS_PER_READ,
)
}
#[allow(clippy::too_many_arguments)]
pub fn query_events_page(
access_mapping: &RoleAccessMapping,
event_types: &[EventType],
tags: Option<&[Tag]>,
after_sequence: Option<i64>,
correlation_id: Option<&str>,
bounded_context_events: &[Event],
resolve_data_key: impl Fn(&str, &str) -> Option<DataKey>,
private_field_grants: &[PrivateFieldGrant],
max_events: usize,
) -> crate::error::Result<Vec<(i64, String)>> {
Ok(query_events_select(
access_mapping,
event_types,
tags,
after_sequence,
correlation_id,
bounded_context_events,
max_events,
)?
.iter()
.map(|e| {
(
e.sequence,
render_event(e, access_mapping, &resolve_data_key, private_field_grants),
)
})
.collect())
}
pub fn query_events_select(
access_mapping: &RoleAccessMapping,
event_types: &[EventType],
tags: Option<&[Tag]>,
after_sequence: Option<i64>,
correlation_id: Option<&str>,
bounded_context_events: &[Event],
max_events: usize,
) -> crate::error::Result<Vec<Event>> {
if access_mapping.status != RoleStatus::Active {
return Err(crate::access_control::Error::GrantNotActive.into());
}
if access_mapping.level != AccessLevel::Admin {
return Err(crate::access_control::Error::InsufficientAccessLevel.into());
}
if !event_types
.iter()
.all(|et| et.bounded_context.same_as(&access_mapping.bounded_context))
{
return Err(Error::EventTypeNotInBoundedContext.into());
}
valid_query_tags(tags)?;
let after = after_sequence.unwrap_or(-1);
Ok(bounded_context_events
.iter()
.filter(|e| e.bounded_context.same_as(&access_mapping.bounded_context))
.filter(|e| {
event_types.is_empty() || event_types.iter().any(|et| et.same_as(&e.event_type))
})
.filter(|e| tags.is_none_or(|wanted| wanted.iter().any(|t| e.tags.contains(t))))
.filter(|e| e.sequence > after)
.filter(|e| {
correlation_id.is_none_or(|id| e.metadata.correlation_id.as_deref() == Some(id))
})
.filter(|e| event_owner_scope_satisfied(e, access_mapping.scope.as_deref()))
.take(max_events)
.cloned()
.collect())
}
pub fn visible_matching_events(
access_mapping: &RoleAccessMapping,
matching_events: &[Event],
max_events: usize,
) -> (Vec<Event>, bool) {
let in_scope: Vec<&Event> = matching_events
.iter()
.filter(|e| event_owner_scope_satisfied(e, access_mapping.scope.as_deref()))
.collect();
let truncated = in_scope.len() > max_events;
let visible = in_scope[in_scope.len().saturating_sub(max_events)..]
.iter()
.map(|e| (*e).clone())
.collect();
(visible, truncated)
}
pub fn count_events(
access_mapping: &RoleAccessMapping,
event_types: &[EventType],
tags: Option<&[Tag]>,
correlation_id: Option<&str>,
bounded_context_events: &[Event],
) -> crate::error::Result<i64> {
if access_mapping.status != RoleStatus::Active {
return Err(crate::access_control::Error::GrantNotActive.into());
}
if access_mapping.level != AccessLevel::Admin {
return Err(crate::access_control::Error::InsufficientAccessLevel.into());
}
if !event_types
.iter()
.all(|et| et.bounded_context.same_as(&access_mapping.bounded_context))
{
return Err(Error::EventTypeNotInBoundedContext.into());
}
valid_query_tags(tags)?;
Ok(bounded_context_events
.iter()
.filter(|e| e.bounded_context.same_as(&access_mapping.bounded_context))
.filter(|e| {
event_types.is_empty() || event_types.iter().any(|et| et.same_as(&e.event_type))
})
.filter(|e| tags.is_none_or(|wanted| wanted.iter().any(|t| e.tags.contains(t))))
.filter(|e| {
correlation_id.is_none_or(|id| e.metadata.correlation_id.as_deref() == Some(id))
})
.filter(|e| event_owner_scope_satisfied(e, access_mapping.scope.as_deref()))
.count() as i64)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EventInspected {
pub event: Event,
pub rendered_payload: String,
}
pub fn inspect_event(
access_mapping: &RoleAccessMapping,
event: &Event,
resolve_data_key: impl Fn(&str, &str) -> Option<DataKey>,
private_field_grants: &[PrivateFieldGrant],
) -> crate::error::Result<EventInspected> {
if access_mapping.status != RoleStatus::Active {
return Err(crate::access_control::Error::GrantNotActive.into());
}
if access_mapping.level != AccessLevel::Admin {
return Err(crate::access_control::Error::InsufficientAccessLevel.into());
}
if !access_mapping
.bounded_context
.same_as(&event.bounded_context)
{
return Err(crate::access_control::Error::GrantBoundedContextMismatch.into());
}
if !event_owner_scope_satisfied(event, access_mapping.scope.as_deref()) {
return Err(crate::access_control::Error::GrantScopeMismatch.into());
}
let mut shown = event.clone();
if let EventOrigin::CommandTriggered { command } = &mut shown.origin {
command.payload = render_command(
command,
access_mapping,
&resolve_data_key,
private_field_grants,
);
}
Ok(EventInspected {
event: shown,
rendered_payload: render_event(
event,
access_mapping,
&resolve_data_key,
private_field_grants,
),
})
}
#[allow(clippy::too_many_arguments)]
pub fn fetch_commands(
access_mapping: &RoleAccessMapping,
command_types: &[CommandType],
after: Option<chrono::DateTime<chrono::Utc>>,
before: Option<chrono::DateTime<chrono::Utc>>,
triggered_event: Option<&Event>,
correlation_id: Option<&str>,
bounded_context_commands: &[Command],
resolve_data_key: impl Fn(&str, &str) -> Option<DataKey>,
private_field_grants: &[PrivateFieldGrant],
) -> crate::error::Result<Vec<String>> {
Ok(fetch_commands_select(
access_mapping,
command_types,
after,
before,
triggered_event,
correlation_id,
None,
bounded_context_commands,
DEFAULT_MAX_EVENTS_PER_READ,
)?
.iter()
.map(|c| render_command(c, access_mapping, &resolve_data_key, private_field_grants))
.collect())
}
#[allow(clippy::too_many_arguments)]
pub fn fetch_commands_select(
access_mapping: &RoleAccessMapping,
command_types: &[CommandType],
after: Option<chrono::DateTime<chrono::Utc>>,
before: Option<chrono::DateTime<chrono::Utc>>,
triggered_event: Option<&Event>,
correlation_id: Option<&str>,
after_command: Option<&Command>,
bounded_context_commands: &[Command],
max_commands: usize,
) -> crate::error::Result<Vec<Command>> {
if access_mapping.status != RoleStatus::Active {
return Err(crate::access_control::Error::GrantNotActive.into());
}
if access_mapping.level != AccessLevel::Admin {
return Err(crate::access_control::Error::InsufficientAccessLevel.into());
}
if !command_types
.iter()
.all(|ct| ct.bounded_context.same_as(&access_mapping.bounded_context))
{
return Err(Error::CommandTypeNotInBoundedContext.into());
}
if triggered_event
.is_some_and(|te| !te.bounded_context.same_as(&access_mapping.bounded_context))
{
return Err(Error::TriggeredEventNotInBoundedContext.into());
}
if after_command.is_some_and(|c| !c.bounded_context.same_as(&access_mapping.bounded_context)) {
return Err(Error::AfterCommandNotInBoundedContext.into());
}
Ok(bounded_context_commands
.iter()
.filter(|c| c.bounded_context.same_as(&access_mapping.bounded_context))
.filter(|c| {
command_types.is_empty() || command_types.iter().any(|ct| ct.same_as(&c.command_type))
})
.filter(|c| after.is_none_or(|a| c.metadata.created_at >= a))
.filter(|c| before.is_none_or(|b| c.metadata.created_at <= b))
.filter(|c| {
triggered_event.is_none_or(|te| match &te.origin {
EventOrigin::CommandTriggered { command } => command.id == c.id,
_ => false,
})
})
.filter(|c| {
correlation_id.is_none_or(|id| c.metadata.correlation_id.as_deref() == Some(id))
})
.filter(|c| command_owner_scope_satisfied(c, access_mapping.scope.as_deref()))
.take(max_commands)
.cloned()
.collect())
}
pub fn create_all_events_subscription(
access_mapping: &RoleAccessMapping,
event_types: Vec<EventType>,
from_sequence: Option<i64>,
bounded_context_events: &[Event],
now: chrono::DateTime<chrono::Utc>,
) -> crate::error::Result<AllEventsSubscription> {
if access_mapping.status != RoleStatus::Active {
return Err(crate::access_control::Error::GrantNotActive.into());
}
if access_mapping.bounded_context.status != BoundedContextStatus::Active {
return Err(Error::BoundedContextArchived.into());
}
if !event_types
.iter()
.all(|et| et.bounded_context.same_as(&access_mapping.bounded_context))
{
return Err(Error::EventTypeNotInBoundedContext.into());
}
let starting_point = from_sequence.unwrap_or_else(|| {
bounded_context_events
.iter()
.filter(|e| e.bounded_context.same_as(&access_mapping.bounded_context))
.map(|e| e.sequence)
.max()
.unwrap_or(-1)
});
Ok(AllEventsSubscription {
bounded_context: access_mapping.bounded_context.clone(),
access_mapping: access_mapping.clone(),
created_at: now,
from_sequence: starting_point,
event_types,
})
}
pub fn create_event_type_subscription(
access_mapping: &RoleAccessMapping,
event_type: &EventType,
filters: Vec<Filter>,
from_sequence: Option<i64>,
bounded_context_events: &[Event],
now: chrono::DateTime<chrono::Utc>,
) -> crate::error::Result<EventTypeSubscription> {
if access_mapping.status != RoleStatus::Active {
return Err(crate::access_control::Error::GrantNotActive.into());
}
if !access_mapping
.bounded_context
.same_as(&event_type.bounded_context)
{
return Err(crate::access_control::Error::GrantBoundedContextMismatch.into());
}
if event_type.bounded_context.status != BoundedContextStatus::Active {
return Err(Error::BoundedContextArchived.into());
}
if !valid_filters(event_type, &filters) {
return Err(Error::InvalidFilter.into());
}
let starting_point = from_sequence.unwrap_or_else(|| {
bounded_context_events
.iter()
.filter(|e| e.bounded_context.same_as(&event_type.bounded_context))
.map(|e| e.sequence)
.max()
.unwrap_or(-1)
});
Ok(EventTypeSubscription {
bounded_context: event_type.bounded_context.clone(),
access_mapping: access_mapping.clone(),
created_at: now,
from_sequence: starting_point,
event_type: event_type.clone(),
filters,
})
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EventDelivered {
pub subscription: Subscription,
pub event: Event,
pub rendered_payload: String,
}
pub fn deliver_to_subscriptions(
event: &Event,
subscriptions: &[Subscription],
resolve_data_key: impl Fn(&str, &str) -> Option<DataKey>,
private_field_grants: &[PrivateFieldGrant],
) -> Vec<EventDelivered> {
subscriptions
.iter()
.filter(|s| subscription_selects(s, event))
.filter(|s| s.access_mapping().status == RoleStatus::Active)
.filter(|s| event_owner_scope_satisfied(event, s.access_mapping().scope.as_deref()))
.map(|s| EventDelivered {
subscription: s.clone(),
event: event.clone(),
rendered_payload: render_event(
event,
s.access_mapping(),
&resolve_data_key,
private_field_grants,
),
})
.collect()
}
pub fn subscription_selects(subscription: &Subscription, event: &Event) -> bool {
subscription
.bounded_context()
.same_as(&event.bounded_context)
&& subscription.starting_sequence() < event.sequence
&& match subscription {
Subscription::AllEventsSubscription(a) => {
a.event_types.is_empty()
|| a.event_types.iter().any(|et| et.same_as(&event.event_type))
}
Subscription::EventTypeSubscription(e) => {
e.event_type.same_as(&event.event_type) && matches_filters(event, &e.filters)
}
}
}
#[derive(Clone)]
pub struct EventBroadcaster {
sender: tokio::sync::broadcast::Sender<Event>,
gaps: std::sync::Arc<tokio::sync::watch::Sender<u64>>,
instance_id: String,
}
impl EventBroadcaster {
pub fn new(capacity: usize) -> Self {
let (sender, _receiver) = tokio::sync::broadcast::channel(capacity.max(1));
let (gaps, _gaps_receiver) = tokio::sync::watch::channel(0);
Self {
sender,
gaps: std::sync::Arc::new(gaps),
instance_id: crate::shared::generate_token_id(),
}
}
pub fn instance_id(&self) -> &str {
&self.instance_id
}
pub fn subscribe(&self) -> tokio::sync::broadcast::Receiver<Event> {
self.sender.subscribe()
}
pub fn publish(&self, event: &Event) {
let _ = self.sender.send(event.clone());
}
pub fn signal_gap(&self) {
self.gaps.send_modify(|n| *n = n.wrapping_add(1));
}
pub fn subscribe_gaps(&self) -> tokio::sync::watch::Receiver<u64> {
self.gaps.subscribe()
}
}
pub fn fetch_events(
token: &EventReadToken,
events: &[Event],
filters: &[Filter],
after_sequence: Option<i64>,
correlation_id: Option<&str>,
) -> crate::error::Result<Vec<Event>> {
fetch_events_page(
token,
events,
filters,
after_sequence,
correlation_id,
DEFAULT_MAX_EVENTS_PER_READ,
)
}
pub fn check_fetch_events(token: &EventReadToken, filters: &[Filter]) -> crate::error::Result<()> {
if token.status != TokenStatus::Active {
return Err(crate::access_control::Error::TokenNotActive.into());
}
let read_type = &token.event_type;
if !read_type.event_read_allowed {
return Err(Error::EventReadNotAllowed.into());
}
if !valid_filters(read_type, filters) {
return Err(Error::InvalidFilter.into());
}
Ok(())
}
pub fn fetch_events_page(
token: &EventReadToken,
events: &[Event],
filters: &[Filter],
after_sequence: Option<i64>,
correlation_id: Option<&str>,
max_events: usize,
) -> crate::error::Result<Vec<Event>> {
check_fetch_events(token, filters)?;
let read_type = &token.event_type;
let after = after_sequence.unwrap_or(-1);
Ok(events
.iter()
.filter(|e| e.bounded_context.same_as(&read_type.bounded_context))
.filter(|e| e.event_type.same_as(read_type))
.filter(|e| e.sequence > after)
.filter(|e| matches_filters(e, filters))
.filter(|e| {
correlation_id.is_none_or(|id| e.metadata.correlation_id.as_deref() == Some(id))
})
.filter(|e| event_owner_scope_satisfied(e, token.scope.as_deref()))
.take(max_events)
.cloned()
.collect())
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum CursorUpdate {
Created(Box<ReadCursor>),
Advanced {
sequence: i64,
updated_at: chrono::DateTime<chrono::Utc>,
},
Claimed {
checked_out_at: chrono::DateTime<chrono::Utc>,
},
Unchanged,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConsumeEventsResult {
pub served: Vec<Event>,
pub cursor_update: CursorUpdate,
}
pub fn consume_events(
token: &EventReadToken,
existing_cursor: Option<&ReadCursor>,
ack_mode: Option<AckMode>,
events: &[Event],
filters: &[Filter],
now: chrono::DateTime<chrono::Utc>,
checkout_lease: chrono::Duration,
) -> crate::error::Result<ConsumeEventsResult> {
let position = existing_cursor.map_or_else(
|| initial_consume_position(token, events),
|cursor| cursor.sequence,
);
let scanned_through = events
.iter()
.filter(|e| e.bounded_context.same_as(&token.event_type.bounded_context))
.filter(|e| e.event_type.same_as(&token.event_type))
.filter(|e| e.sequence > position)
.map(|e| e.sequence)
.max();
consume_events_page(
token,
existing_cursor,
ack_mode,
position,
events,
scanned_through,
filters,
now,
checkout_lease,
DEFAULT_MAX_EVENTS_PER_READ,
)
}
pub fn initial_consume_position<'a>(
token: &EventReadToken,
history: impl IntoIterator<Item = &'a Event>,
) -> i64 {
let read_type = &token.event_type;
let qualifying = history
.into_iter()
.filter(|e| e.bounded_context.same_as(&read_type.bounded_context))
.filter(|e| e.event_type.same_as(read_type))
.filter(|e| event_owner_scope_satisfied(e, token.scope.as_deref()));
match token.start_from {
EventReadStartPosition::Beginning => -1,
EventReadStartPosition::Latest => qualifying.map(|e| e.sequence).max().unwrap_or(-1),
EventReadStartPosition::AtSequence => token.start_at_sequence.expect(
"create_event_read_token guarantees start_at_sequence is Some when \
start_from = AtSequence",
),
EventReadStartPosition::AtTime => {
let threshold = token.start_at_time.expect(
"create_event_read_token guarantees start_at_time is Some when \
start_from = AtTime",
);
qualifying
.filter(|e| e.metadata.created_at <= threshold)
.map(|e| e.sequence)
.max()
.unwrap_or(-1)
}
}
}
pub fn check_consume_events(
token: &EventReadToken,
existing_cursor: Option<&ReadCursor>,
ack_mode: Option<AckMode>,
filters: &[Filter],
) -> crate::error::Result<()> {
check_fetch_events(token, filters)?;
match (existing_cursor, ack_mode) {
(None, None) => Err(Error::CursorAckModeMismatch.into()),
(Some(cursor), Some(requested)) if requested != cursor.ack_mode => {
Err(Error::CursorAckModeMismatch.into())
}
_ => Ok(()),
}
}
#[allow(clippy::too_many_arguments)]
pub fn consume_events_page(
token: &EventReadToken,
existing_cursor: Option<&ReadCursor>,
ack_mode: Option<AckMode>,
new_cursor_position: i64,
events: &[Event],
scanned_through: Option<i64>,
filters: &[Filter],
now: chrono::DateTime<chrono::Utc>,
checkout_lease: chrono::Duration,
max_events: usize,
) -> crate::error::Result<ConsumeEventsResult> {
check_consume_events(token, existing_cursor, ack_mode, filters)?;
let read_type = &token.event_type;
let is_new = existing_cursor.is_none();
let mode = ack_mode
.or_else(|| existing_cursor.map(|c| c.ack_mode))
.expect("is_new implies ack_mode.is_some(), checked above");
let position = existing_cursor.map_or(new_cursor_position, |cursor| cursor.sequence);
let is_leased = mode == AckMode::ManualAck
&& !is_new
&& existing_cursor
.and_then(|c| c.checked_out_at)
.is_some_and(|checked_out_at| checked_out_at + checkout_lease > now);
let served: Vec<Event> = if is_leased {
Vec::new()
} else {
events
.iter()
.filter(|e| e.bounded_context.same_as(&read_type.bounded_context))
.filter(|e| e.event_type.same_as(read_type))
.filter(|e| e.sequence > position)
.filter(|e| matches_filters(e, filters))
.filter(|e| event_owner_scope_satisfied(e, token.scope.as_deref()))
.take(max_events)
.cloned()
.collect()
};
let caught_up = (served.len() < max_events).then(|| {
position
.max(highest_sequence(&served).unwrap_or(position))
.max(scanned_through.unwrap_or(position))
});
let next_position = match mode {
AckMode::AutoAdvance => {
caught_up.unwrap_or_else(|| highest_sequence(&served).unwrap_or(position))
}
AckMode::ManualAck if !is_leased && served.is_empty() => caught_up.unwrap_or(position),
AckMode::ManualAck => position,
};
let cursor_update = if is_new {
CursorUpdate::Created(Box::new(ReadCursor {
token: token.clone(),
ack_mode: mode,
sequence: next_position,
checked_out_at: (mode == AckMode::ManualAck && !served.is_empty()).then_some(now),
updated_at: now,
}))
} else if mode == AckMode::AutoAdvance {
CursorUpdate::Advanced {
sequence: next_position,
updated_at: now,
}
} else if !is_leased && served.is_empty() {
if next_position == position {
CursorUpdate::Unchanged
} else {
CursorUpdate::Advanced {
sequence: next_position,
updated_at: now,
}
}
} else if !is_leased {
CursorUpdate::Claimed {
checked_out_at: now,
}
} else {
CursorUpdate::Unchanged
};
Ok(ConsumeEventsResult {
served,
cursor_update,
})
}
pub fn acknowledge_events(
token: &EventReadToken,
cursor: Option<&ReadCursor>,
sequence: i64,
now: chrono::DateTime<chrono::Utc>,
) -> crate::error::Result<(i64, chrono::DateTime<chrono::Utc>)> {
if token.status != TokenStatus::Active {
return Err(crate::access_control::Error::TokenNotActive.into());
}
let cursor = cursor.ok_or(Error::NoReadCursor)?;
if cursor.ack_mode != AckMode::ManualAck {
return Err(Error::NotManualAckCursor.into());
}
if sequence < cursor.sequence {
return Err(Error::AcknowledgementRegresses.into());
}
Ok((sequence, now))
}
pub fn check_create_external_event(
adapter: &ExternalEventToken,
payload: &str,
correlation_id: Option<&str>,
causation_id: Option<&str>,
blind: &BlindIndex,
) -> crate::error::Result<Vec<Tag>> {
if adapter.status != TokenStatus::Active {
return Err(crate::access_control::Error::TokenNotActive.into());
}
if !adapter.event_type.external_creation_allowed {
return Err(Error::ExternalCreationNotAllowed.into());
}
check_event_creation(
&adapter.event_type,
adapter.scope.as_deref(),
payload,
correlation_id,
causation_id,
blind,
)
}
pub fn check_create_direct_event(
adapter: &DirectCreationToken,
payload: &str,
correlation_id: Option<&str>,
causation_id: Option<&str>,
blind: &BlindIndex,
) -> crate::error::Result<Vec<Tag>> {
if adapter.status != TokenStatus::Active {
return Err(crate::access_control::Error::TokenNotActive.into());
}
if !adapter.event_type.direct_creation_allowed {
return Err(Error::DirectCreationNotAllowed.into());
}
check_event_creation(
&adapter.event_type,
adapter.scope.as_deref(),
payload,
correlation_id,
causation_id,
blind,
)
}
fn check_event_creation(
event_type: &EventType,
scope: Option<&str>,
payload: &str,
correlation_id: Option<&str>,
causation_id: Option<&str>,
blind: &BlindIndex,
) -> crate::error::Result<Vec<Tag>> {
if event_type.bounded_context.status != BoundedContextStatus::Active {
return Err(Error::BoundedContextArchived.into());
}
if !valid_payload(&event_type.schema, payload) {
return Err(Error::PayloadDoesNotMatchSchema.into());
}
if !valid_correlation_id(correlation_id) || !valid_correlation_id(causation_id) {
return Err(Error::CorrelationIdTooLong.into());
}
let tags = derive_tags(
&event_type.tag_mappings,
payload,
blind,
&event_type.bounded_context.name,
)?;
if !tag_owner_scope_satisfied(&tags, event_type.owner_tag_key.as_deref(), scope) {
return Err(crate::access_control::Error::GrantScopeMismatch.into());
}
Ok(tags)
}
#[allow(clippy::too_many_arguments)]
pub fn create_external_event(
adapter: &ExternalEventToken,
payload: String,
source_content: String,
source_context: Option<String>,
correlation_id: Option<String>,
causation_id: Option<String>,
next_sequence: i64,
now: chrono::DateTime<chrono::Utc>,
resolve_key: impl Fn(&str, &str) -> (EncryptionKey, DataKey),
blind: &BlindIndex,
) -> crate::error::Result<Event> {
let tags = check_create_external_event(
adapter,
&payload,
correlation_id.as_deref(),
causation_id.as_deref(),
blind,
)?;
let event_type = &adapter.event_type;
let protected = protect_sensitive_fields(&event_type.sensitive_fields, &payload, resolve_key);
Ok(Event {
bounded_context: event_type.bounded_context.clone(),
event_type: event_type.clone(),
payload: protected.payload,
metadata: Metadata {
r#type: event_type.name.clone(),
version: event_type.schema_version,
client_id: adapter.id.clone(),
created_at: now,
correlation_id: Some(
correlation_id
.filter(|id| !id.is_empty())
.unwrap_or_else(crate::shared::generate_token_id),
),
causation_id: causation_id.filter(|id| !id.is_empty()),
},
sequence: next_sequence,
tags,
encryption_keys: protected.encryption_keys,
origin: EventOrigin::ExternalTriggered {
source_content,
source_context,
},
})
}
#[allow(clippy::too_many_arguments)]
pub fn create_direct_event(
adapter: &DirectCreationToken,
payload: String,
correlation_id: Option<String>,
causation_id: Option<String>,
next_sequence: i64,
now: chrono::DateTime<chrono::Utc>,
resolve_key: impl Fn(&str, &str) -> (EncryptionKey, DataKey),
blind: &BlindIndex,
) -> crate::error::Result<Event> {
let tags = check_create_direct_event(
adapter,
&payload,
correlation_id.as_deref(),
causation_id.as_deref(),
blind,
)?;
let event_type = &adapter.event_type;
let protected = protect_sensitive_fields(&event_type.sensitive_fields, &payload, resolve_key);
Ok(Event {
bounded_context: event_type.bounded_context.clone(),
event_type: event_type.clone(),
payload: protected.payload,
metadata: Metadata {
r#type: event_type.name.clone(),
version: event_type.schema_version,
client_id: adapter.id.clone(),
created_at: now,
correlation_id: Some(
correlation_id
.filter(|id| !id.is_empty())
.unwrap_or_else(crate::shared::generate_token_id),
),
causation_id: causation_id.filter(|id| !id.is_empty()),
},
sequence: next_sequence,
tags,
encryption_keys: protected.encryption_keys,
origin: EventOrigin::DirectlyCreated,
})
}
pub fn next_occurrence_after(
schedule: &str,
instant: chrono::DateTime<chrono::Utc>,
) -> Option<chrono::DateTime<chrono::Utc>> {
use std::str::FromStr;
cron::Schedule::from_str(schedule)
.ok()?
.after(&instant)
.next()
}
pub fn latest_occurrence_at_or_before(
schedule: &str,
instant: chrono::DateTime<chrono::Utc>,
) -> Option<chrono::DateTime<chrono::Utc>> {
use std::str::FromStr;
let schedule = cron::Schedule::from_str(schedule).ok()?;
let mut latest = schedule.after(&instant).next_back()?;
while let Some(next) = schedule.after(&latest).next() {
if next > instant {
break;
}
latest = next;
}
(latest <= instant).then_some(latest)
}
#[allow(clippy::too_many_arguments)]
pub fn create_system_event(
event_type: &EventType,
occurrence_at: chrono::DateTime<chrono::Utc>,
now: chrono::DateTime<chrono::Utc>,
next_sequence: i64,
resolve_payload: impl FnOnce() -> String,
resolve_key: impl Fn(&str, &str) -> (EncryptionKey, DataKey),
) -> Option<(Event, chrono::DateTime<chrono::Utc>)> {
if !event_type.system_triggered_allowed {
return None;
}
if event_type.bounded_context.status != BoundedContextStatus::Active {
return None;
}
let schedule = event_type.system_triggered_schedule.as_deref()?;
let policy = event_type.missed_occurrence_policy?;
let position = event_type.schedule_position?;
if occurrence_at > now || occurrence_at <= position {
return None;
}
let no_gap = next_occurrence_after(schedule, position) == Some(occurrence_at);
let nothing_later_is_due =
next_occurrence_after(schedule, occurrence_at).is_none_or(|next| next > now);
let eligible = match policy {
MissedOccurrencePolicy::ReplayBacklog => true,
MissedOccurrencePolicy::FireOnce => nothing_later_is_due,
MissedOccurrencePolicy::Skip => no_gap && nothing_later_is_due,
};
if !eligible {
return None;
}
let payload = resolve_payload();
let protected = protect_sensitive_fields(&event_type.sensitive_fields, &payload, resolve_key);
let event = Event {
bounded_context: event_type.bounded_context.clone(),
event_type: event_type.clone(),
payload: protected.payload,
metadata: Metadata {
r#type: event_type.name.clone(),
version: event_type.schema_version,
client_id: "system".to_string(),
created_at: now,
correlation_id: Some(crate::shared::generate_token_id()),
causation_id: None,
},
sequence: next_sequence,
tags: derive_plain_tags(&event_type.tag_mappings, &payload),
encryption_keys: protected.encryption_keys,
origin: EventOrigin::SystemTriggered,
};
Some((event, occurrence_at))
}
pub fn skip_missed_occurrences(
event_type: &EventType,
now: chrono::DateTime<chrono::Utc>,
) -> Option<chrono::DateTime<chrono::Utc>> {
if !event_type.system_triggered_allowed {
return None;
}
if event_type.missed_occurrence_policy != Some(MissedOccurrencePolicy::Skip) {
return None;
}
if event_type.bounded_context.status != BoundedContextStatus::Active {
return None;
}
let position = event_type.schedule_position?;
if position >= now {
return None;
}
Some(now)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CommandAuthorised {
pub command_type: CommandType,
pub payload: String,
pub client_id: String,
pub correlation_id: Option<String>,
pub causation_id: Option<String>,
}
pub fn authorise_command_trigger(
token: &CommandToken,
payload: String,
correlation_id: Option<String>,
causation_id: Option<String>,
) -> crate::error::Result<CommandAuthorised> {
if token.status != TokenStatus::Active {
return Err(crate::access_control::Error::TokenNotActive.into());
}
let command_type = &token.command_type;
if !command_type.rest_trigger_allowed {
return Err(Error::RestTriggerNotAllowed.into());
}
if command_type.bounded_context.status != BoundedContextStatus::Active {
return Err(Error::BoundedContextArchived.into());
}
if !valid_payload(&command_type.schema, &payload) {
return Err(Error::PayloadDoesNotMatchSchema.into());
}
let owner_tags = derive_plain_tags(&command_type.tag_mappings, &payload);
if !tag_owner_scope_satisfied(
&owner_tags,
command_type.owner_tag_key.as_deref(),
token.scope.as_deref(),
) {
return Err(crate::access_control::Error::GrantScopeMismatch.into());
}
Ok(CommandAuthorised {
command_type: command_type.clone(),
payload,
client_id: token.id.clone(),
correlation_id,
causation_id,
})
}
pub fn authorise_command_submission(
access_mapping: &RoleAccessMapping,
command_type: &CommandType,
payload: String,
correlation_id: Option<String>,
causation_id: Option<String>,
) -> crate::error::Result<CommandAuthorised> {
if access_mapping.status != RoleStatus::Active {
return Err(crate::access_control::Error::GrantNotActive.into());
}
if !matches!(
access_mapping.level,
AccessLevel::Write | AccessLevel::Admin
) {
return Err(crate::access_control::Error::InsufficientAccessLevel.into());
}
if !access_mapping
.bounded_context
.same_as(&command_type.bounded_context)
{
return Err(crate::access_control::Error::GrantBoundedContextMismatch.into());
}
if command_type.bounded_context.status != BoundedContextStatus::Active {
return Err(Error::BoundedContextArchived.into());
}
if !valid_payload(&command_type.schema, &payload) {
return Err(Error::PayloadDoesNotMatchSchema.into());
}
let owner_tags = derive_plain_tags(&command_type.tag_mappings, &payload);
if !tag_owner_scope_satisfied(
&owner_tags,
command_type.owner_tag_key.as_deref(),
access_mapping.scope.as_deref(),
) {
return Err(crate::access_control::Error::GrantScopeMismatch.into());
}
Ok(CommandAuthorised {
command_type: command_type.clone(),
payload,
client_id: access_mapping.role.id.clone(),
correlation_id,
causation_id,
})
}
pub fn authorise_command_dry_run(
access_mapping: &RoleAccessMapping,
command_type: &CommandType,
payload: String,
) -> crate::error::Result<CommandAuthorised> {
if access_mapping.status != RoleStatus::Active {
return Err(crate::access_control::Error::GrantNotActive.into());
}
if access_mapping.level != AccessLevel::Admin {
return Err(crate::access_control::Error::InsufficientAccessLevel.into());
}
authorise_command_submission(access_mapping, command_type, payload, None, None)
}
pub fn consistency_boundary_and_matching_events(
bounded_context_events: &[Event],
consistency_tags: &[Tag],
) -> (Option<i64>, Vec<Event>) {
let matching: Vec<Event> = bounded_context_events
.iter()
.filter(|e| consistency_tags.iter().any(|t| e.tags.contains(t)))
.cloned()
.collect();
let boundary = highest_sequence(&matching);
(boundary, matching)
}
pub fn consistency_boundary(
bounded_context_events: &[Event],
consistency_tags: &[Tag],
) -> Option<i64> {
bounded_context_events
.iter()
.filter(|e| consistency_tags.iter().any(|t| e.tags.contains(t)))
.map(|e| e.sequence)
.max()
}
impl QueryItem {
pub fn matches(&self, event: &Event) -> bool {
if self.event_types.is_empty() && self.tags.is_empty() {
return false;
}
(self.event_types.is_empty() || self.event_types.contains(&event.event_type.name))
&& self.tags.iter().all(|tag| event.tags.contains(tag))
}
}
pub fn derive_query(
mappings: &[QueryItemMapping],
payload: &str,
blind: &BlindIndex,
bounded_context: &str,
) -> crate::error::Result<Vec<QueryItem>> {
let mut items = Vec::with_capacity(mappings.len());
for mapping in mappings {
if mapping.snapshot.is_some() {
let tags: Vec<Tag> =
derive_tags(&mapping.tag_mappings, payload, blind, bounded_context)?
.into_iter()
.filter(|tag| tag.value.is_some())
.collect();
if tags.is_empty() {
items.push(QueryItem {
event_types: Vec::new(),
tags: Vec::new(),
latest: None,
snapshot: mapping.snapshot.clone(),
});
}
items.extend(tags.into_iter().map(|tag| QueryItem {
event_types: Vec::new(),
tags: vec![tag],
latest: None,
snapshot: mapping.snapshot.clone(),
}));
continue;
}
let mut underived = false;
for m in &mapping.tag_mappings {
underived |=
derive_tags(std::slice::from_ref(m), payload, blind, bounded_context)?.is_empty();
}
if underived {
items.push(QueryItem {
event_types: Vec::new(),
tags: Vec::new(),
latest: mapping.latest,
snapshot: None,
});
continue;
}
items.push(QueryItem {
event_types: mapping.event_types.iter().map(|t| t.to_string()).collect(),
tags: derive_tags(&mapping.tag_mappings, payload, blind, bounded_context)?,
latest: mapping.latest,
snapshot: None,
});
}
Ok(items)
}
pub fn event_items(query: &[QueryItem]) -> Vec<QueryItem> {
query
.iter()
.filter(|item| item.snapshot.is_none())
.cloned()
.collect()
}
pub fn query_has_snapshots(query: &[QueryItem]) -> bool {
query
.iter()
.any(|item| item.snapshot.is_some() && !item.tags.is_empty())
}
pub fn effective_query(
consistency_query: &[QueryItem],
consistency_tags: &[Tag],
) -> Vec<QueryItem> {
if !consistency_query.is_empty() {
return consistency_query.to_vec();
}
consistency_tags
.iter()
.map(|tag| QueryItem {
event_types: Vec::new(),
tags: vec![tag.clone()],
latest: None,
snapshot: None,
})
.collect()
}
pub fn query_reads_nothing(query: &[QueryItem]) -> bool {
!query.is_empty()
&& query
.iter()
.all(|item| item.event_types.is_empty() && item.tags.is_empty())
}
pub fn query_matches(query: &[QueryItem], event: &Event) -> bool {
query.iter().any(|item| item.matches(event))
}
fn query_selection(query: &[QueryItem], events: &[Event]) -> Vec<bool> {
if query.iter().all(|item| item.latest.is_none()) {
return events.iter().map(|e| query_matches(query, e)).collect();
}
let mut keep = vec![false; events.len()];
for item in query {
if let Some(count) = item.latest {
let mut found = 0;
for (index, event) in events.iter().enumerate().rev() {
if found >= count {
break;
}
if item.matches(event) {
keep[index] = true;
found += 1;
}
}
} else {
for (index, event) in events.iter().enumerate() {
if !keep[index] && item.matches(event) {
keep[index] = true;
}
}
}
}
keep
}
pub fn select_matching_events(query: &[QueryItem], events: &[Event]) -> Vec<Event> {
query_selection(query, events)
.into_iter()
.zip(events)
.filter(|(keep, _)| *keep)
.map(|(_, event)| event.clone())
.collect()
}
pub fn query_boundary(query: &[QueryItem], events: &[Event]) -> Option<i64> {
if query.iter().all(|item| item.latest.is_none()) {
return events
.iter()
.filter(|e| query_matches(query, e))
.map(|e| e.sequence)
.max();
}
query_selection(query, events)
.into_iter()
.zip(events)
.filter_map(|(keep, event)| keep.then_some(event.sequence))
.max()
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProcessCommandResult {
pub command: Command,
pub events: Vec<Event>,
}
#[allow(clippy::too_many_arguments)]
#[tracing::instrument(
skip_all,
fields(
bounded_context = %command_type.bounded_context.name,
command_type = %command_type.name,
)
)]
pub fn process_command(
id: String,
command_type: &CommandType,
payload: &str,
client_id: &str,
correlation_id: Option<&str>,
causation_id: Option<&str>,
bounded_context_events: &[Event],
decision: CommandDecision,
resolve_event_type: impl Fn(&str) -> Option<EventType>,
next_sequence: impl FnMut() -> i64,
now: chrono::DateTime<chrono::Utc>,
resolve_key: impl Fn(&str, &str) -> (EncryptionKey, DataKey),
blind: &BlindIndex,
) -> crate::error::Result<ProcessCommandResult> {
let boundary = consistency_boundary(
bounded_context_events,
&derive_tags(
&command_type.tag_mappings,
payload,
blind,
&command_type.bounded_context.name,
)?,
);
process_command_at_boundary(
id,
command_type,
payload,
client_id,
correlation_id,
causation_id,
Vec::new(),
boundary,
decision,
resolve_event_type,
next_sequence,
now,
resolve_key,
blind,
)
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn process_command_at_boundary(
id: String,
command_type: &CommandType,
payload: &str,
client_id: &str,
correlation_id: Option<&str>,
causation_id: Option<&str>,
consistency_query: Vec<QueryItem>,
consistency_boundary: Option<i64>,
decision: CommandDecision,
resolve_event_type: impl Fn(&str) -> Option<EventType>,
mut next_sequence: impl FnMut() -> i64,
now: chrono::DateTime<chrono::Utc>,
resolve_key: impl Fn(&str, &str) -> (EncryptionKey, DataKey),
blind: &BlindIndex,
) -> crate::error::Result<ProcessCommandResult> {
if !valid_correlation_id(correlation_id) || !valid_correlation_id(causation_id) {
return Err(Error::CorrelationIdTooLong.into());
}
if command_type.bounded_context.status != BoundedContextStatus::Active {
return Err(Error::BoundedContextArchived.into());
}
let event_specs = match decision {
CommandDecision::Accepted { events } => events,
CommandDecision::Rejected { reason, kind } => {
return Err(crate::error::Error::CommandRejected { reason, kind });
}
};
let correlation_id = correlation_id
.filter(|id| !id.is_empty())
.map(str::to_string)
.unwrap_or_else(crate::shared::generate_token_id);
let causation_id = causation_id.filter(|id| !id.is_empty()).map(str::to_string);
let bounded_context = &command_type.bounded_context.name;
let consistency_tags =
derive_tags(&command_type.tag_mappings, payload, blind, bounded_context)?;
let protected = protect_sensitive_fields(&command_type.sensitive_fields, payload, &resolve_key);
let command = Command {
id,
bounded_context: command_type.bounded_context.clone(),
command_type: command_type.clone(),
payload: protected.payload,
metadata: Metadata {
r#type: command_type.name.clone(),
version: command_type.schema_version,
client_id: client_id.to_string(),
created_at: now,
correlation_id: Some(correlation_id),
causation_id,
},
encryption_keys: protected.encryption_keys,
consistency_tags,
consistency_query,
consistency_boundary,
};
let mut events = Vec::with_capacity(event_specs.len());
for spec in event_specs {
let event_type = resolve_event_type(&spec.event_type)
.ok_or_else(|| Error::UnregisteredEventType(spec.event_type.clone()))?;
let spec_payload = spec.payload.to_string();
let event_protected =
protect_sensitive_fields(&event_type.sensitive_fields, &spec_payload, &resolve_key);
events.push(Event {
bounded_context: command_type.bounded_context.clone(),
event_type: event_type.clone(),
payload: event_protected.payload,
metadata: Metadata {
r#type: event_type.name.clone(),
version: event_type.schema_version,
client_id: client_id.to_string(),
created_at: now,
correlation_id: command.metadata.correlation_id.clone(),
causation_id: Some(command.id.clone()),
},
sequence: next_sequence(),
tags: derive_tags(
&event_type.tag_mappings,
&spec_payload,
blind,
bounded_context,
)?,
encryption_keys: event_protected.encryption_keys,
origin: EventOrigin::CommandTriggered {
command: Box::new(command.clone()),
},
});
}
Ok(ProcessCommandResult { command, events })
}