Skip to content

Commit

Permalink
Added remaining Organization/EndUser APIs. #4
Browse files Browse the repository at this point in the history
  • Loading branch information
jezzsantos committed Apr 26, 2024
1 parent fbd9dc6 commit 014677c
Show file tree
Hide file tree
Showing 129 changed files with 4,313 additions and 796 deletions.
15 changes: 8 additions & 7 deletions docs/design-principles/0000-all-use-cases.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ These are the main use cases of this product that are exposed via "public" APIs
3. Invite a guest to register on the platform (a referral)
4. Resend an invitation to a guest
5. Guest verifies an invitation is still valid
6. Change the default organization for the current (Authenticated) user
7. List all memberships of the current (Authenticated) user

### Identities

Expand Down Expand Up @@ -110,16 +112,15 @@ These are the main use cases of this product that are exposed via "public" APIs

1. Create a new (shared) organization for the current user
2. Inspect a specific organization
3. (coming soon) Change the organization's details
3. Change the organization's details
4. Add an Avatar image to the organization
5. Remove the Avatar from the organization
6. Invite another guest or person to an organization (guest by email, or an existing person by email or by ID)
7. (coming soon) Uninvite a member of the organization
8. (coming soon) Assign roles to a member
9. (coming soon) Unassign roles from a member
10. (coming soon) List all members of the organization
11. (coming soon) List all memberships of the current (Authenticated) user
12. (coming soon) Delete the organization
7. Un-invite a member from the organization
8. Assign roles to a member
9. Unassign roles from a member
10. List all members of the organization
11. Delete the organization (must be no remaining members)

### Subscriptions

Expand Down
17 changes: 12 additions & 5 deletions docs/design-principles/0170-eventing.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,18 +89,25 @@ Another advantage (only available to event-sourced persistence scheme) is that w
### Event Notifications

Notifications are the mechanism by which subdomains can communicate to other subdomains (or to other processes) about what is happening in the source subdomain. This means that a source subdomain does not have to directly instruct another [dependent] target subdomain to update its state, when the source subdomain state changes. Typically, this is done by a direct synchronous method/API call. Now the target domain can simply react to the appearance of a "domain event" from the source subdomain, and take appropriate action. The coupling of the method/API call is gone.
Notifications are the mechanism by which subdomains can communicate to other subdomains (or to other processes) about what is happening in the source subdomain. This means that a source subdomain does not have to [imperatively] instruct another [dependent] target subdomain to update its state, when the source subdomain state changes. Typically, this is done by a direct synchronous method/API call.

> This is particularly useful when you have highly inter-dependent subdomains, that require that their data be in sync with each other (i.e., `EndUser` memberships with `Organizations`.
Instead, the target domain can simply "observe" and react to the appearance of a "domain event" from the source subdomain, and take appropriate action.

This characteristic is particularly necessary in distributed deployments, where direct calls are HTTP calls, requiring both the source and target subdomains to be responsive to each other.
The coupling of the imperative method/API call is eliminated.

Instead, this decoupling via "integration events" would normally done in distributed systems with a message broker of some kind (i.e., a queue, a message bus, etc.).
> This is particularly useful when you have highly inter-dependent subdomains, that require that their data be in sync with each other (i.e., `EndUser` memberships with `Organizations` and `UserProfiles`. As seen below.
![Generic Subdomains](../images/Event Flows - Generic.png)

This eventing capability is particularly necessary in distributed deployments, where direct calls between separately deployed components are realized as HTTP calls (requiring both the source and target subdomains to be synchronously responsive and consistent to each other).

Instead, decoupling this asynchronously via "integration events" would normally done in distributed systems with a message broker of some kind (i.e., a queue, a message bus, etc.).

The synchronous publication of all "domain events" is handled automatically by the `IEventNotifyingStoreNotificationRelay` (after events have first been projected by the `IEventNotifyingStoreProjectionRelay`).

Domain events are published synchronously (round-robin) one at a time:
Domain/Integration events are published synchronously (round-robin) one at a time:

1. First, to all registered `IDomainEventNotificationConsumer` consumers. These consumers can fail and report back errors that are captured synchronously.
2. Then to all registered `IIntegrationEventNotificationTranslator` translators, that have the option to translate a "domain event" into an "integration event" or not. This translation can also fail, and report back errors that are captured synchronously.
3. Finally, if the translator translates a "domain event" into an "integration event" it is then published to the `IEventNotificationMessageBroker` that should send the "integration event" to some external message broker, who will deliver it asynchronous to external consumers. This can also fail, and report back errors that are captured synchronously.

Binary file added docs/images/Event Flows - Generic.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/Physical-Architecture-AWS.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/Physical-Architecture-Azure.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/Ports-And-Adapters.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/Sources.pptx
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
using Application.Persistence.Common.Extensions;
using Application.Persistence.Interfaces;
using Common;
using Domain.Common.ValueObjects;
using Domain.Events.Shared.Ancillary.Audits;
using Domain.Interfaces;
using Domain.Interfaces.Entities;
Expand All @@ -29,7 +28,7 @@ public async Task<Result<bool, Error>> ProjectEventAsync(IDomainEvent changeEven
switch (changeEvent)
{
case Created e:
return await _audits.HandleCreateAsync(e.RootId.ToId(), dto =>
return await _audits.HandleCreateAsync(e.RootId, dto =>
{
dto.OrganizationId = e.OrganizationId;
dto.AuditCode = e.AuditCode;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
using Application.Persistence.Common.Extensions;
using Application.Persistence.Interfaces;
using Common;
using Domain.Common.ValueObjects;
using Domain.Events.Shared.Ancillary.EmailDelivery;
using Domain.Interfaces;
using Domain.Interfaces.Entities;
Expand All @@ -29,7 +28,7 @@ public async Task<Result<bool, Error>> ProjectEventAsync(IDomainEvent changeEven
switch (changeEvent)
{
case Created e:
return await _deliveries.HandleCreateAsync(e.RootId.ToId(), dto =>
return await _deliveries.HandleCreateAsync(e.RootId, dto =>
{
dto.MessageId = e.MessageId;
dto.Attempts = DeliveryAttempts.Empty;
Expand All @@ -40,7 +39,7 @@ public async Task<Result<bool, Error>> ProjectEventAsync(IDomainEvent changeEven
cancellationToken);

case EmailDetailsChanged e:
return await _deliveries.HandleUpdateAsync(e.RootId.ToId(), dto =>
return await _deliveries.HandleUpdateAsync(e.RootId, dto =>
{
dto.Subject = e.Subject;
dto.Body = e.Body;
Expand All @@ -49,7 +48,7 @@ public async Task<Result<bool, Error>> ProjectEventAsync(IDomainEvent changeEven
}, cancellationToken);

case DeliveryAttempted e:
return await _deliveries.HandleUpdateAsync(e.RootId.ToId(), dto =>
return await _deliveries.HandleUpdateAsync(e.RootId, dto =>
{
var attempts = dto.Attempts.HasValue
? dto.Attempts.Value.Attempt(e.When)
Expand All @@ -67,11 +66,11 @@ public async Task<Result<bool, Error>> ProjectEventAsync(IDomainEvent changeEven
}, cancellationToken);

case DeliveryFailed e:
return await _deliveries.HandleUpdateAsync(e.RootId.ToId(), dto => { dto.Failed = e.When; },
return await _deliveries.HandleUpdateAsync(e.RootId, dto => { dto.Failed = e.When; },
cancellationToken);

case DeliverySucceeded e:
return await _deliveries.HandleUpdateAsync(e.RootId.ToId(), dto => { dto.Delivered = e.When; },
return await _deliveries.HandleUpdateAsync(e.RootId, dto => { dto.Delivered = e.When; },
cancellationToken);

default:
Expand Down
9 changes: 9 additions & 0 deletions src/Application.Interfaces/Audits.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/Application.Interfaces/Audits.resx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@
<data name="EndUserApplication_TenantRolesAssigned" xml:space="preserve">
<value>EndUser.TenantRolesAssigned</value>
</data>
<data name="EndUserApplication_TenantRolesUnassigned" xml:space="preserve">
<value>EndUser.TenantRolesUnassigned</value>
</data>
<data name="EndUserApplication_PlatformRolesAssigned" xml:space="preserve">
<value>EndUser.PlatformRolesAssigned</value>
</data>
Expand Down
10 changes: 6 additions & 4 deletions src/Application.Resources.Shared/EndUser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,18 +53,20 @@ public class Membership : IIdentifiableResource

public required string OrganizationId { get; set; }

public List<string> Roles { get; set; } = new();
public OrganizationOwnership Ownership { get; set; }

public required string Id { get; set; }
public List<string> Roles { get; set; } = new();

public required string UserId { get; set; }

public required string Id { get; set; }
}

public class MembershipWithUserProfile : Membership
{
public EndUserStatus Status { get; set; }

public required UserProfile Profile { get; set; }

public EndUserStatus Status { get; set; }
}

public class Invitation
Expand Down
2 changes: 1 addition & 1 deletion src/Application.Resources.Shared/UserProfile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public class UserProfile : IIdentifiableResource
public required string Id { get; set; }
}

public class UserProfileForCurrent : UserProfileWithDefaultMembership
public class UserProfileForCaller : UserProfileWithDefaultMembership
{
public List<string> Features { get; set; } = new();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
using CarsApplication.Persistence.ReadModels;
using CarsDomain;
using Common;
using Domain.Common.ValueObjects;
using Domain.Events.Shared.Cars;
using Domain.Interfaces;
using Domain.Interfaces.Entities;
Expand Down Expand Up @@ -32,7 +31,7 @@ public async Task<Result<bool, Error>> ProjectEventAsync(IDomainEvent changeEven
switch (changeEvent)
{
case Created e:
return await _cars.HandleCreateAsync(e.RootId.ToId(), dto =>
return await _cars.HandleCreateAsync(e.RootId, dto =>
{
dto.OrganizationId = e.OrganizationId;
dto.Status = e.Status;
Expand Down
20 changes: 19 additions & 1 deletion src/Common/Error.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,15 @@ public Error Wrap(ErrorCode code, string message)
}
#endif

/// <summary>
/// Whether this error is of the specified <see cref="code" />
/// and optional <see cref="message" />
/// </summary>
public bool Is(ErrorCode code, string? message = null)
{
return code == Code && (message == null || message == Message);
}

/// <summary>
/// Creates a <see cref="ErrorCode.NoError" /> error
/// </summary>
Expand Down Expand Up @@ -144,6 +153,14 @@ public static Error Unexpected(string? message = null)
return new Error(ErrorCode.Unexpected, message);
}

/// <summary>
/// Creates a <see cref="ErrorCode.EntityDeleted" /> error
/// </summary>
public static Error EntityDeleted(string? message = null)
{
return new Error(ErrorCode.EntityDeleted, message);
}

public override string ToString()
{
return $"{Code}: {Message}";
Expand All @@ -167,5 +184,6 @@ public enum ErrorCode
NotAuthenticated,
ForbiddenAccess,
NotSubscribed,
Unexpected
Unexpected,
EntityDeleted
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@

namespace Domain.Events.Shared.EndUsers;

public sealed class MembershipDefaultChanged : DomainEvent
public sealed class DefaultMembershipChanged : DomainEvent
{
public MembershipDefaultChanged(Identifier id) : base(id)
public DefaultMembershipChanged(Identifier id) : base(id)
{
}

[UsedImplicitly]
public MembershipDefaultChanged()
public DefaultMembershipChanged()
{
}

Expand Down
23 changes: 23 additions & 0 deletions src/Domain.Events.Shared/EndUsers/MembershipRemoved.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using Domain.Common;
using Domain.Common.ValueObjects;
using JetBrains.Annotations;

namespace Domain.Events.Shared.EndUsers;

public sealed class MembershipRemoved : DomainEvent
{
public MembershipRemoved(Identifier id) : base(id)
{
}

[UsedImplicitly]
public MembershipRemoved()
{
}

public required string MembershipId { get; set; }

public required string OrganizationId { get; set; }

public required string UnInvitedById { get; set; }
}
23 changes: 23 additions & 0 deletions src/Domain.Events.Shared/EndUsers/MembershipRoleUnassigned.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using Domain.Common;
using Domain.Common.ValueObjects;
using JetBrains.Annotations;

namespace Domain.Events.Shared.EndUsers;

public sealed class MembershipRoleUnassigned : DomainEvent
{
public MembershipRoleUnassigned(Identifier id) : base(id)
{
}

[UsedImplicitly]
public MembershipRoleUnassigned()
{
}

public required string MembershipId { get; set; }

public required string OrganizationId { get; set; }

public required string Role { get; set; }
}
23 changes: 23 additions & 0 deletions src/Domain.Events.Shared/Organizations/MemberInvited.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using Domain.Common;
using Domain.Common.ValueObjects;
using JetBrains.Annotations;

namespace Domain.Events.Shared.Organizations;

public sealed class MemberInvited : DomainEvent
{
public MemberInvited(Identifier id) : base(id)
{
}

[UsedImplicitly]
public MemberInvited()
{
}

public string? EmailAddress { get; set; }

public required string InvitedById { get; set; }

public string? UserId { get; set; }
}
21 changes: 21 additions & 0 deletions src/Domain.Events.Shared/Organizations/MemberUnInvited.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using Domain.Common;
using Domain.Common.ValueObjects;
using JetBrains.Annotations;

namespace Domain.Events.Shared.Organizations;

public sealed class MemberUnInvited : DomainEvent
{
public MemberUnInvited(Identifier id) : base(id)
{
}

[UsedImplicitly]
public MemberUnInvited()
{
}

public required string UninvitedById { get; set; }

public required string UserId { get; set; }
}
6 changes: 1 addition & 5 deletions src/Domain.Events.Shared/Organizations/MembershipAdded.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,5 @@ public MembershipAdded()
{
}

public string? EmailAddress { get; set; }

public required string InvitedById { get; set; }

public string? UserId { get; set; }
public required string UserId { get; set; }
}
19 changes: 19 additions & 0 deletions src/Domain.Events.Shared/Organizations/MembershipRemoved.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using Domain.Common;
using Domain.Common.ValueObjects;
using JetBrains.Annotations;

namespace Domain.Events.Shared.Organizations;

public sealed class MembershipRemoved : DomainEvent
{
public MembershipRemoved(Identifier id) : base(id)
{
}

[UsedImplicitly]
public MembershipRemoved()
{
}

public required string UserId { get; set; }
}
Loading

0 comments on commit 014677c

Please sign in to comment.