Projection with RemoveWithJoin
The RemovedWithJoin<>() method allows you to remove child items from collections when events from other streams indicate that related data should be removed. This is particularly useful in scenarios where the removal event occurs on a different stream than the one that originally added the child.
Understanding RemoveWithJoin vs RemovedWith
Section titled “Understanding RemoveWithJoin vs RemovedWith”RemovedWith<>(): Removes children based on events from the same streamRemovedWithJoin<>(): Removes children based on events from different streams (joins)
Basic RemoveWithJoin usage
Section titled “Basic RemoveWithJoin usage”Use RemovedWithJoin<>() in child projections to specify which events should trigger child removal:
using Cratis.Chronicle.Events;using Cratis.Chronicle.Projections;
[EventType]public record DecRemoveWithJoinBasicUserCreated(string Name);
[EventType]public record DecRemoveWithJoinBasicUserAddedToGroup(string UserId, string GroupId);
[EventType]public record DecRemoveWithJoinBasicGroupCreated(string Name);
[EventType]public record DecRemoveWithJoinBasicGroupDeleted;
public record DecRemoveWithJoinBasicUserGroup(string GroupId, string Name, DateTimeOffset JoinedAt);
public record DecRemoveWithJoinBasicUser(string Name, IEnumerable<DecRemoveWithJoinBasicUserGroup> Groups);
public class DecRemoveWithJoinBasicUserProjection : IProjectionFor<DecRemoveWithJoinBasicUser>{ public void Define(IProjectionBuilderFor<DecRemoveWithJoinBasicUser> builder) => builder .From<DecRemoveWithJoinBasicUserCreated>() .Children(m => m.Groups, children => children .IdentifiedBy(e => e.GroupId) .From<DecRemoveWithJoinBasicUserAddedToGroup>(_ => _ .UsingParentKey(e => e.UserId) .Set(m => m.JoinedAt).ToEventContextProperty(c => c.Occurred)) .Join<DecRemoveWithJoinBasicGroupCreated>() .RemovedWithJoin<DecRemoveWithJoinBasicGroupDeleted>());}Kotlin does not support this workflow yet.`IChildrenBuilderFor`/`IChildFromBuilderFor` have no `removedWithJoin` method — removal via a joinedevent only exists for the whole read model instance (`IProjectionBuilderFor.removedWithJoin`), notfor a single item inside a `children()` collection.Java does not support this workflow yet.`IChildrenBuilderFor`/`IChildFromBuilderFor` have no `removedWithJoin` method — removal via a joinedevent only exists for the whole read model instance (`IProjectionBuilderFor.removedWithJoin`), notfor a single item inside a `children()` collection.defmodule MyApp.Projections.DecRwjUserWithGroupsProjection do use Chronicle.Projections.Projection, model: MyApp.ReadModels.DecRwjUserWithGroups
alias MyApp.Events.{DecRwjUserAddedToGroup, DecRwjGroupDeleted}
children :groups, identified_by: :group_id do from DecRwjUserAddedToGroup, set: [group_id: :group_id]
# removed_with_join defaults its key to the event source id, so GroupDeleted's own event # source id (the group's id) is matched against every user's membership entry with the # same group_id — the event never has to come from this user's own event stream. removed_with_join DecRwjGroupDeleted endendimport { eventType, IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle';import { field } from '@cratis/fundamentals';
@eventType()class DecRemoveWithJoinBasicUserCreated { @field(String) readonly name: string;
constructor(name: string) { this.name = name; }}
@eventType()class DecRemoveWithJoinBasicUserAddedToGroup { @field(String) readonly userId: string; @field(String) readonly groupId: string;
constructor(userId: string, groupId: string) { this.userId = userId; this.groupId = groupId; }}
@eventType()class DecRemoveWithJoinBasicGroupCreated { @field(String) readonly name: string;
constructor(name: string) { this.name = name; }}
@eventType()class DecRemoveWithJoinBasicGroupDeleted {}
class DecRemoveWithJoinBasicUserGroup { groupId = ''; name = ''; joinedAt = new Date();}
class DecRemoveWithJoinBasicUser { name = ''; groups: DecRemoveWithJoinBasicUserGroup[] = [];}
@projection()class DecRemoveWithJoinBasicUserProjection implements IProjectionFor<DecRemoveWithJoinBasicUser> { define(builder: IProjectionBuilderFor<DecRemoveWithJoinBasicUser>): void { builder .autoMap() .from(DecRemoveWithJoinBasicUserCreated) .children<DecRemoveWithJoinBasicUserGroup>(m => m.groups, children => children .identifiedBy(e => e.groupId) .autoMap() .from(DecRemoveWithJoinBasicUserAddedToGroup, _ => _ .usingParentKey(e => e.userId) .set(m => m.joinedAt).toEventContextProperty('occurred')) .join(DecRemoveWithJoinBasicGroupCreated, _ => _ .on(m => m.groupId)) .removedWithJoin(DecRemoveWithJoinBasicGroupDeleted)); }}In this example:
- When a
UserAddedToGroupevent occurs, a group is added to the user’s collection UsingParentKey(e => e.UserId)extracts the parent (user) identifier from the event content- When a
GroupDeletedevent occurs anywhere in the system, that group is removed from all users - The removal is based on the group ID that was used to join the data
Note: If you don’t specify
UsingParentKey(), the framework uses the EventSourceId as the parent identifier by default. UseUsingParentKey()when the parent identifier is a property in the event content rather than the EventSourceId.
How RemoveWithJoin works
Section titled “How RemoveWithJoin works”When using RemovedWithJoin<>():
- Event occurs: The specified event type (e.g.,
GroupDeleted) is processed - Key extraction: The system extracts the key from the event source ID or specified key expression
- Child lookup: All projections with children joined on that key are found
- Removal: The matching child items are removed from all affected collections
RemoveWithJoin with explicit keys
Section titled “RemoveWithJoin with explicit keys”You can specify which property to use as the key for removal:
using Cratis.Chronicle.Events;using Cratis.Chronicle.Projections;
[EventType]public record DecRemoveWithJoinExplicitEmployeeHired(string Name);
[EventType]public record DecRemoveWithJoinExplicitEmployeeAssignedToProject(string EmployeeId, string ProjectId);
[EventType]public record DecRemoveWithJoinExplicitProjectCreated(string Name);
[EventType]public record DecRemoveWithJoinExplicitProjectCancelled(string ProjectId);
public record DecRemoveWithJoinExplicitEmployeeProject(string ProjectId, string Name, DateTimeOffset AssignedAt);
public record DecRemoveWithJoinExplicitEmployee(string Name, IEnumerable<DecRemoveWithJoinExplicitEmployeeProject> Projects);
public class DecRemoveWithJoinExplicitEmployeeProjection : IProjectionFor<DecRemoveWithJoinExplicitEmployee>{ public void Define(IProjectionBuilderFor<DecRemoveWithJoinExplicitEmployee> builder) => builder .From<DecRemoveWithJoinExplicitEmployeeHired>() .Children(m => m.Projects, children => children .IdentifiedBy(e => e.ProjectId) .From<DecRemoveWithJoinExplicitEmployeeAssignedToProject>(_ => _ .UsingParentKey(e => e.EmployeeId) .UsingKey(e => e.ProjectId) .Set(m => m.AssignedAt).ToEventContextProperty(c => c.Occurred)) .Join<DecRemoveWithJoinExplicitProjectCreated>(_ => _ .On(m => m.ProjectId)) .RemovedWithJoin<DecRemoveWithJoinExplicitProjectCancelled>(_ => _ .UsingKey(e => e.ProjectId)));}Kotlin does not support this workflow yet.`IChildrenBuilderFor`/`IChildFromBuilderFor` have no `removedWithJoin` method — removal via a joinedevent only exists for the whole read model instance (`IProjectionBuilderFor.removedWithJoin`), notfor a single item inside a `children()` collection.Java does not support this workflow yet.`IChildrenBuilderFor`/`IChildFromBuilderFor` have no `removedWithJoin` method — removal via a joinedevent only exists for the whole read model instance (`IProjectionBuilderFor.removedWithJoin`), notfor a single item inside a `children()` collection.defmodule MyApp.Projections.DecRwjUserWithGroupsExplicitKeyProjection do use Chronicle.Projections.Projection, model: MyApp.ReadModels.DecRwjUserWithGroupsExplicitKey
alias MyApp.Events.{DecRwjUserAddedToGroup, DecRwjGroupDeletedWithKey}
children :groups, identified_by: :group_id do from DecRwjUserAddedToGroup, set: [group_id: :group_id]
# key: names the event property that identifies which membership to remove, for when it # is not the removing event's own event source id. removed_with_join DecRwjGroupDeletedWithKey, key: :deleted_group_id endendimport { eventType, IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle';import { field } from '@cratis/fundamentals';
@eventType()class DecRemoveWithJoinExplicitEmployeeHired { @field(String) readonly name: string;
constructor(name: string) { this.name = name; }}
@eventType()class DecRemoveWithJoinExplicitEmployeeAssignedToProject { @field(String) readonly employeeId: string; @field(String) readonly projectId: string;
constructor(employeeId: string, projectId: string) { this.employeeId = employeeId; this.projectId = projectId; }}
@eventType()class DecRemoveWithJoinExplicitProjectCreated { @field(String) readonly name: string;
constructor(name: string) { this.name = name; }}
@eventType()class DecRemoveWithJoinExplicitProjectCancelled { @field(String) readonly projectId: string;
constructor(projectId: string) { this.projectId = projectId; }}
class DecRemoveWithJoinExplicitEmployeeProject { projectId = ''; name = ''; assignedAt = new Date();}
class DecRemoveWithJoinExplicitEmployee { name = ''; projects: DecRemoveWithJoinExplicitEmployeeProject[] = [];}
@projection()class DecRemoveWithJoinExplicitEmployeeProjection implements IProjectionFor<DecRemoveWithJoinExplicitEmployee> { define(builder: IProjectionBuilderFor<DecRemoveWithJoinExplicitEmployee>): void { builder .autoMap() .from(DecRemoveWithJoinExplicitEmployeeHired) .children<DecRemoveWithJoinExplicitEmployeeProject>(m => m.projects, children => children .identifiedBy(e => e.projectId) .autoMap() .from(DecRemoveWithJoinExplicitEmployeeAssignedToProject, _ => _ .usingParentKey(e => e.employeeId) .usingKey(e => e.projectId) .set(m => m.assignedAt).toEventContextProperty('occurred')) .join(DecRemoveWithJoinExplicitProjectCreated, _ => _ .on(m => m.projectId)) .removedWithJoin(DecRemoveWithJoinExplicitProjectCancelled, _ => _ .usingKey(e => e.projectId))); }}Real-world example: User groups
Section titled “Real-world example: User groups”Consider a system where users can be members of groups, and groups can be deleted:
public class DecRemoveWithJoinGroupMembershipProjection : IProjectionFor<DecRemoveWithJoinUserProfile>{ public void Define(IProjectionBuilderFor<DecRemoveWithJoinUserProfile> builder) => builder .From<DecRemoveWithJoinUserRegistered>(_ => _ .Set(m => m.UserId).ToEventSourceId() .Set(m => m.RegisteredAt).ToEventContextProperty(c => c.Occurred)) .Children(m => m.Memberships, children => children .IdentifiedBy(e => e.GroupId) .From<DecRemoveWithJoinUserJoinedGroup>(_ => _ .UsingParentKey(e => e.UserId) .UsingKey(e => e.GroupId) .Set(m => m.JoinedAt).ToEventContextProperty(c => c.Occurred)) .Join<DecRemoveWithJoinGroupCreated>(_ => _ .On(m => m.GroupId)) .RemovedWith<DecRemoveWithJoinUserLeftGroup>(_ => _ .UsingParentKey(e => e.UserId) .UsingKey(e => e.GroupId)) .RemovedWithJoin<DecRemoveWithJoinGroupDisbanded>());}Kotlin does not support this workflow yet.`IChildrenBuilderFor`/`IChildFromBuilderFor` have no `removedWith`/`removedWithJoin` method — removinga single item from a child collection, whether from the same stream or a joined one, is only possibletoday with model-bound `@RemovedWith`/`@RemovedWithJoin` on a `@ChildrenFrom` property.Java does not support this workflow yet.`IChildrenBuilderFor`/`IChildFromBuilderFor` have no `removedWith`/`removedWithJoin` method — removinga single item from a child collection, whether from the same stream or a joined one, is only possibletoday with model-bound `@RemovedWith`/`@RemovedWithJoin` on a `@ChildrenFrom` property.defmodule MyApp.Events.DecRwjUserRegistered do use Chronicle.Events.EventType, id: "dec-rwj-user-registered"
defstruct [:username, :email]end
defmodule MyApp.Events.DecRwjUserJoinedGroup do use Chronicle.Events.EventType, id: "dec-rwj-user-joined-group"
defstruct [:user_id, :group_id, :role]end
defmodule MyApp.Events.DecRwjUserLeftGroup do use Chronicle.Events.EventType, id: "dec-rwj-user-left-group"
defstruct [:user_id, :group_id]end
defmodule MyApp.Events.DecRwjGroupCreated do use Chronicle.Events.EventType, id: "dec-rwj-group-created"
defstruct [:group_name, :group_type]end
defmodule MyApp.Events.DecRwjGroupDisbanded do use Chronicle.Events.EventType, id: "dec-rwj-group-disbanded"
defstruct []end
defmodule MyApp.ReadModels.DecRwjUserGroupMembership do defstruct group_id: nil, group_name: nil, group_type: nil, joined_at: nil, role: nilend
defmodule MyApp.ReadModels.DecRwjUserProfile do use Chronicle.ReadModels.ReadModel, types: [memberships: {:list, MyApp.ReadModels.DecRwjUserGroupMembership}]
defstruct user_id: nil, username: nil, email: nil, registered_at: nil, memberships: []end
defmodule MyApp.Projections.DecRwjGroupMembershipProjection do use Chronicle.Projections.Projection, model: MyApp.ReadModels.DecRwjUserProfile
alias MyApp.Events.{ DecRwjGroupCreated, DecRwjGroupDisbanded, DecRwjUserJoinedGroup, DecRwjUserLeftGroup, DecRwjUserRegistered }
from DecRwjUserRegistered, set: [user_id: :event_source_id, registered_at: :occurred]
children :memberships, identified_by: :group_id do from DecRwjUserJoinedGroup, parent_key: :user_id, key: :group_id, set: [group_id: :group_id, role: :role, joined_at: :occurred]
join DecRwjGroupCreated, set: [group_name: :group_name, group_type: :group_type]
removed_with DecRwjUserLeftGroup, parent_key: :user_id, key: :group_id
# The group's own event source id identifies every membership entry to remove. removed_with_join DecRwjGroupDisbanded endendimport { IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle';
@projection()class DecRemoveWithJoinGroupMembershipProjection implements IProjectionFor<DecRemoveWithJoinUserProfile> { define(builder: IProjectionBuilderFor<DecRemoveWithJoinUserProfile>): void { builder .autoMap() .from(DecRemoveWithJoinUserRegistered, _ => _ .set(m => m.userId).toEventSourceId() .set(m => m.registeredAt).toEventContextProperty('occurred')) .children<DecRemoveWithJoinGroupMembership>(m => m.memberships, children => children .identifiedBy(e => e.groupId) .autoMap() .from(DecRemoveWithJoinUserJoinedGroup, _ => _ .usingParentKey(e => e.userId) .usingKey(e => e.groupId) .set(m => m.joinedAt).toEventContextProperty('occurred')) .join(DecRemoveWithJoinGroupCreated, _ => _ .on(m => m.groupId)) .removedWith(DecRemoveWithJoinUserLeftGroup, _ => _ .usingKey(e => e.groupId)) .removedWithJoin(DecRemoveWithJoinGroupDisbanded)); }}In this example:
RemovedWith<UserLeftGroup>: Removes membership when a user explicitly leaves a groupRemovedWithJoin<GroupDisbanded>: Removes membership from all users when a group is disbanded
Complex scenario: Project assignments
Section titled “Complex scenario: Project assignments”public class DecRemoveWithJoinDeveloperProjectsProjection : IProjectionFor<DecRemoveWithJoinDeveloperProfile>{ public void Define(IProjectionBuilderFor<DecRemoveWithJoinDeveloperProfile> builder) => builder .From<DecRemoveWithJoinDeveloperOnboarded>(_ => _ .Set(m => m.DeveloperId).ToEventSourceId() .Set(m => m.OnboardedAt).ToEventContextProperty(c => c.Occurred)) .Children(m => m.CurrentProjects, children => children .IdentifiedBy(e => e.ProjectId) .From<DecRemoveWithJoinDeveloperAssignedToProject>(_ => _ .UsingParentKey(e => e.DeveloperId) .UsingKey(e => e.ProjectId) .Set(m => m.AssignedAt).ToEventContextProperty(c => c.Occurred)) .Join<DecRemoveWithJoinProjectInitiated>(_ => _ .On(m => m.ProjectId)) .RemovedWith<DecRemoveWithJoinDeveloperUnassignedFromProject>(_ => _ .UsingParentKey(e => e.DeveloperId) .UsingKey(e => e.ProjectId)) .RemovedWithJoin<DecRemoveWithJoinProjectCancelled>() .RemovedWithJoin<DecRemoveWithJoinProjectCompleted>());}Kotlin does not support this workflow yet.`IChildrenBuilderFor`/`IChildFromBuilderFor` have no `removedWith`/`removedWithJoin` method — removinga single item from a child collection, whether from the same stream or a joined one, is only possibletoday with model-bound `@RemovedWith`/`@RemovedWithJoin` on a `@ChildrenFrom` property.Java does not support this workflow yet.`IChildrenBuilderFor`/`IChildFromBuilderFor` have no `removedWith`/`removedWithJoin` method — removinga single item from a child collection, whether from the same stream or a joined one, is only possibletoday with model-bound `@RemovedWith`/`@RemovedWithJoin` on a `@ChildrenFrom` property.defmodule MyApp.Events.DecRwjDeveloperOnboarded do use Chronicle.Events.EventType, id: "dec-rwj-developer-onboarded"
defstruct [:name, skills: []]end
defmodule MyApp.Events.DecRwjDeveloperAssignedToProject do use Chronicle.Events.EventType, id: "dec-rwj-developer-assigned-to-project"
defstruct [:developer_id, :project_id, :role, allocation: 0]end
defmodule MyApp.Events.DecRwjDeveloperUnassignedFromProject do use Chronicle.Events.EventType, id: "dec-rwj-developer-unassigned-from-project"
defstruct [:developer_id, :project_id]end
defmodule MyApp.Events.DecRwjProjectInitiated do use Chronicle.Events.EventType, id: "dec-rwj-project-initiated"
defstruct [:project_name, :priority, :deadline]end
defmodule MyApp.Events.DecRwjProjectCancelled do use Chronicle.Events.EventType, id: "dec-rwj-project-cancelled"
defstruct []end
defmodule MyApp.Events.DecRwjProjectCompleted do use Chronicle.Events.EventType, id: "dec-rwj-project-completed"
defstruct []end
defmodule MyApp.ReadModels.DecRwjProjectAssignment do defstruct project_id: nil, project_name: nil, priority: nil, deadline: nil, assigned_at: nil, role: nil, allocation: 0end
defmodule MyApp.ReadModels.DecRwjDeveloperProfile do use Chronicle.ReadModels.ReadModel, types: [current_projects: {:list, MyApp.ReadModels.DecRwjProjectAssignment}]
defstruct developer_id: nil, name: nil, onboarded_at: nil, current_projects: []end
defmodule MyApp.Projections.DecRwjDeveloperProjectsProjection do use Chronicle.Projections.Projection, model: MyApp.ReadModels.DecRwjDeveloperProfile
alias MyApp.Events.{ DecRwjDeveloperAssignedToProject, DecRwjDeveloperOnboarded, DecRwjDeveloperUnassignedFromProject, DecRwjProjectCancelled, DecRwjProjectCompleted, DecRwjProjectInitiated }
from DecRwjDeveloperOnboarded, set: [developer_id: :event_source_id, onboarded_at: :occurred]
children :current_projects, identified_by: :project_id do from DecRwjDeveloperAssignedToProject, parent_key: :developer_id, key: :project_id, set: [project_id: :project_id, role: :role, allocation: :allocation, assigned_at: :occurred]
join DecRwjProjectInitiated, set: [project_name: :project_name, priority: :priority, deadline: :deadline]
removed_with DecRwjDeveloperUnassignedFromProject, parent_key: :developer_id, key: :project_id
removed_with_join DecRwjProjectCancelled removed_with_join DecRwjProjectCompleted endendimport { IProjectionBuilderFor, IProjectionFor, projection } from '@cratis/chronicle';
@projection()class DecRemoveWithJoinDeveloperProjectsProjection implements IProjectionFor<DecRemoveWithJoinDeveloperProfile> { define(builder: IProjectionBuilderFor<DecRemoveWithJoinDeveloperProfile>): void { builder .autoMap() .from(DecRemoveWithJoinDeveloperOnboarded, _ => _ .set(m => m.developerId).toEventSourceId() .set(m => m.onboardedAt).toEventContextProperty('occurred')) .children<DecRemoveWithJoinProjectAssignment>(m => m.currentProjects, children => children .identifiedBy(e => e.projectId) .autoMap() .from(DecRemoveWithJoinDeveloperAssignedToProject, _ => _ .usingParentKey(e => e.developerId) .usingKey(e => e.projectId) .set(m => m.assignedAt).toEventContextProperty('occurred')) .join(DecRemoveWithJoinProjectInitiated, _ => _ .on(m => m.projectId)) .removedWith(DecRemoveWithJoinDeveloperUnassignedFromProject, _ => _ .usingKey(e => e.projectId)) .removedWithJoin(DecRemoveWithJoinProjectCancelled) .removedWithJoin(DecRemoveWithJoinProjectCompleted)); }}This handles three removal scenarios:
- Individual unassignment:
DeveloperUnassignedFromProjectremoves one developer from one project - Project cancellation:
ProjectCancelledremoves the project from all developers - Project completion:
ProjectCompletedremoves the project from all developers
Read model examples
Section titled “Read model examples”public record DecRemoveWithJoinUserProfile( string UserId, string Username, string Email, DateTimeOffset RegisteredAt, IEnumerable<DecRemoveWithJoinGroupMembership> Memberships);
public record DecRemoveWithJoinGroupMembership( string GroupId, string GroupName, string GroupType, DateTimeOffset JoinedAt, string Role);
public record DecRemoveWithJoinDeveloperProfile( string DeveloperId, string Name, IEnumerable<string> Skills, DateTimeOffset OnboardedAt, IEnumerable<DecRemoveWithJoinProjectAssignment> CurrentProjects);
public record DecRemoveWithJoinProjectAssignment( string ProjectId, string ProjectName, string Priority, DateTimeOffset Deadline, DateTimeOffset AssignedAt, string Role, int Allocation);import java.time.Instant
data class DecRemoveWithJoinUserProfile( val userId: String = "", val username: String = "", val email: String = "", val registeredAt: Instant = Instant.EPOCH, val memberships: List<DecRemoveWithJoinGroupMembership> = emptyList())
data class DecRemoveWithJoinGroupMembership( val groupId: String = "", val groupName: String = "", val groupType: String = "", val joinedAt: Instant = Instant.EPOCH, val role: String = "")
data class DecRemoveWithJoinDeveloperProfile( val developerId: String = "", val name: String = "", val skills: List<String> = emptyList(), val onboardedAt: Instant = Instant.EPOCH, val currentProjects: List<DecRemoveWithJoinProjectAssignment> = emptyList())
data class DecRemoveWithJoinProjectAssignment( val projectId: String = "", val projectName: String = "", val priority: String = "", val deadline: Instant = Instant.EPOCH, val assignedAt: Instant = Instant.EPOCH, val role: String = "", val allocation: Int = 0)import java.time.Instant;import java.util.List;
record DecRemoveWithJoinUserProfile( String userId, String username, String email, Instant registeredAt, List<DecRemoveWithJoinGroupMembership> memberships) {}
record DecRemoveWithJoinGroupMembership( String groupId, String groupName, String groupType, Instant joinedAt, String role) {}
record DecRemoveWithJoinDeveloperProfile( String developerId, String name, List<String> skills, Instant onboardedAt, List<DecRemoveWithJoinProjectAssignment> currentProjects) {}
record DecRemoveWithJoinProjectAssignment( String projectId, String projectName, String priority, Instant deadline, Instant assignedAt, String role, int allocation) {}defmodule MyApp.ReadModels.DecRwjGroupMembership do defstruct group_id: nilend
defmodule MyApp.ReadModels.DecRwjUserWithGroups do use Chronicle.ReadModels.ReadModel, types: [groups: {:list, MyApp.ReadModels.DecRwjGroupMembership}]
defstruct user_id: nil, groups: []end
defmodule MyApp.ReadModels.DecRwjUserWithGroupsExplicitKey do use Chronicle.ReadModels.ReadModel, types: [groups: {:list, MyApp.ReadModels.DecRwjGroupMembership}]
defstruct user_id: nil, groups: []endclass DecRemoveWithJoinGroupMembership { groupId = ''; groupName = ''; groupType = ''; joinedAt = new Date(); role = '';}
class DecRemoveWithJoinUserProfile { userId = ''; username = ''; email = ''; registeredAt = new Date(); memberships: DecRemoveWithJoinGroupMembership[] = [];}
class DecRemoveWithJoinProjectAssignment { projectId = ''; projectName = ''; priority = ''; deadline = new Date(); assignedAt = new Date(); role = ''; allocation = 0;}
class DecRemoveWithJoinDeveloperProfile { developerId = ''; name = ''; skills: string[] = []; onboardedAt = new Date(); currentProjects: DecRemoveWithJoinProjectAssignment[] = [];}Event definitions
Section titled “Event definitions”using Cratis.Chronicle.Events;
[EventType]public record DecRemoveWithJoinUserRegistered(string Username, string Email);
[EventType]public record DecRemoveWithJoinUserJoinedGroup(string UserId, string GroupId, string Role);
[EventType]public record DecRemoveWithJoinUserLeftGroup(string UserId, string GroupId);
[EventType]public record DecRemoveWithJoinGroupCreated(string GroupName, string GroupType);
[EventType]public record DecRemoveWithJoinGroupDisbanded;
[EventType]public record DecRemoveWithJoinDeveloperOnboarded(string Name, IEnumerable<string> Skills);
[EventType]public record DecRemoveWithJoinDeveloperAssignedToProject(string DeveloperId, string ProjectId, string Role, int Allocation);
[EventType]public record DecRemoveWithJoinDeveloperUnassignedFromProject(string DeveloperId, string ProjectId);
[EventType]public record DecRemoveWithJoinProjectInitiated(string ProjectName, string Priority, DateTimeOffset Deadline);
[EventType]public record DecRemoveWithJoinProjectCancelled;
[EventType]public record DecRemoveWithJoinProjectCompleted;import io.cratis.chronicle.events.EventType
@EventTypedata class DecRemoveWithJoinUserRegistered(val username: String, val email: String)
@EventTypedata class DecRemoveWithJoinUserJoinedGroup(val userId: String, val groupId: String, val role: String)
@EventTypedata class DecRemoveWithJoinUserLeftGroup(val userId: String, val groupId: String)
@EventTypedata class DecRemoveWithJoinGroupCreated(val groupName: String, val groupType: String)
@EventTypedata class DecRemoveWithJoinGroupDisbanded(val placeholder: Boolean = true)
@EventTypedata class DecRemoveWithJoinDeveloperOnboarded(val name: String, val skills: List<String>)
@EventTypedata class DecRemoveWithJoinDeveloperAssignedToProject( val developerId: String, val projectId: String, val role: String, val allocation: Int)
@EventTypedata class DecRemoveWithJoinDeveloperUnassignedFromProject(val developerId: String, val projectId: String)
@EventTypedata class DecRemoveWithJoinProjectInitiated( val projectName: String, val priority: String, val deadline: java.time.Instant)
@EventTypedata class DecRemoveWithJoinProjectCancelled(val placeholder: Boolean = true)
@EventTypedata class DecRemoveWithJoinProjectCompleted(val placeholder: Boolean = true)import io.cratis.chronicle.events.EventType;
import java.time.Instant;import java.util.List;
@EventTyperecord DecRemoveWithJoinUserRegistered(String username, String email) {}
@EventTyperecord DecRemoveWithJoinUserJoinedGroup(String userId, String groupId, String role) {}
@EventTyperecord DecRemoveWithJoinUserLeftGroup(String userId, String groupId) {}
@EventTyperecord DecRemoveWithJoinGroupCreated(String groupName, String groupType) {}
@EventTyperecord DecRemoveWithJoinGroupDisbanded() {}
@EventTyperecord DecRemoveWithJoinDeveloperOnboarded(String name, List<String> skills) {}
@EventTyperecord DecRemoveWithJoinDeveloperAssignedToProject( String developerId, String projectId, String role, int allocation) {}
@EventTyperecord DecRemoveWithJoinDeveloperUnassignedFromProject(String developerId, String projectId) {}
@EventTyperecord DecRemoveWithJoinProjectInitiated(String projectName, String priority, Instant deadline) {}
@EventTyperecord DecRemoveWithJoinProjectCancelled() {}
@EventTyperecord DecRemoveWithJoinProjectCompleted() {}defmodule MyApp.Events.DecRwjUserAddedToGroup do use Chronicle.Events.EventType, id: "dec-rwj-user-added-to-group"
defstruct [:group_id]end
defmodule MyApp.Events.DecRwjGroupDeleted do use Chronicle.Events.EventType, id: "dec-rwj-group-deleted"
defstruct []end
defmodule MyApp.Events.DecRwjGroupDeletedWithKey do use Chronicle.Events.EventType, id: "dec-rwj-group-deleted-with-key"
defstruct [:deleted_group_id]endimport { eventType } from '@cratis/chronicle';import { field } from '@cratis/fundamentals';
@eventType()class DecRemoveWithJoinUserRegistered { @field(String) readonly username: string; @field(String) readonly email: string;
constructor(username: string, email: string) { this.username = username; this.email = email; }}
@eventType()class DecRemoveWithJoinUserJoinedGroup { @field(String) readonly userId: string; @field(String) readonly groupId: string; @field(String) readonly role: string;
constructor(userId: string, groupId: string, role: string) { this.userId = userId; this.groupId = groupId; this.role = role; }}
@eventType()class DecRemoveWithJoinUserLeftGroup { @field(String) readonly userId: string; @field(String) readonly groupId: string;
constructor(userId: string, groupId: string) { this.userId = userId; this.groupId = groupId; }}
@eventType()class DecRemoveWithJoinGroupCreated { @field(String) readonly groupName: string; @field(String) readonly groupType: string;
constructor(groupName: string, groupType: string) { this.groupName = groupName; this.groupType = groupType; }}
@eventType()class DecRemoveWithJoinGroupDisbanded {}
@eventType()class DecRemoveWithJoinDeveloperOnboarded { @field(String) readonly name: string; @field(Array, { genericArguments: [String] }) readonly skills: string[];
constructor(name: string, skills: string[]) { this.name = name; this.skills = skills; }}
@eventType()class DecRemoveWithJoinDeveloperAssignedToProject { @field(String) readonly developerId: string; @field(String) readonly projectId: string; @field(String) readonly role: string; @field(Number) readonly allocation: number;
constructor(developerId: string, projectId: string, role: string, allocation: number) { this.developerId = developerId; this.projectId = projectId; this.role = role; this.allocation = allocation; }}
@eventType()class DecRemoveWithJoinDeveloperUnassignedFromProject { @field(String) readonly developerId: string; @field(String) readonly projectId: string;
constructor(developerId: string, projectId: string) { this.developerId = developerId; this.projectId = projectId; }}
@eventType()class DecRemoveWithJoinProjectInitiated { @field(String) readonly projectName: string; @field(String) readonly priority: string; @field(Date) readonly deadline: Date;
constructor(projectName: string, priority: string, deadline: Date) { this.projectName = projectName; this.priority = priority; this.deadline = deadline; }}
@eventType()class DecRemoveWithJoinProjectCancelled {}
@eventType()class DecRemoveWithJoinProjectCompleted {}Best practices
Section titled “Best practices”Use for cross-stream cleanup
Section titled “Use for cross-stream cleanup”RemovedWithJoin<>() is ideal when:
- Events from one stream should trigger cleanup in projections of other stream
- You need to maintain referential integrity across stream boundaries
- Deletion or deactivation events should cascade to related read models
Combine with RemovedWith
Section titled “Combine with RemovedWith”Often you’ll use both removal methods together:
RemovedWith<>()for explicit removals within the same streamRemovedWithJoin<>()for cascade removals from related streams
Consider event ordering
Section titled “Consider event ordering”Be aware that RemovedWithJoin<>() events might be processed before all related Join<>() events have completed, so ensure your system handles partial data gracefully.
The RemovedWithJoin<>() method provides powerful cross-stream cleanup capabilities, ensuring that your read models stay consistent when related data is removed from other parts of your system.