Skip to content

Commit

Permalink
docs: architecture docs (#230)
Browse files Browse the repository at this point in the history
  • Loading branch information
jbrunton authored Sep 23, 2024
1 parent f5898f5 commit 6eb7a13
Show file tree
Hide file tree
Showing 38 changed files with 123 additions and 36 deletions.
19 changes: 19 additions & 0 deletions pnpm-lock.yaml

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

60 changes: 60 additions & 0 deletions services/api/architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Domain Architecture

The domain is organised around these fundamental concepts:

- {@link domain/entities/users/user | Users} represent authenticated users (or the {@link domain/entities/users/system-user | system user}).

- {@link domain/entities/messages/message | Messages} refer to the messages sent by users and by the system.

- {@link domain/entities/rooms/room | Rooms} are the message rooms users may join, to which messages are sent.

- {@link domain/entities/memberships/membership | Memberships} represent the membership status history (`PendingApproval`, `Joined`, `Revoked`, etc.) of a user for a given room. This history is used to authorize users using an {@link domain/usecases/auth-service | AuthService}.

## Messages

The uniquely identifiable **entity** representing a message in the system is a {@link domain/entities/messages/message!SentMessage | SentMessage}. Before being sent, there are a few **value objects** which represent how messages are processed and dispatched:

1. An {@link domain/entities/messages/message!IncomingMessage | IncomingMessage} represents a new message received by the system. At this point it has not been processed or stored.
2. A message prefixed with a forward slash (e.g. `/help`) is considered to be a command, represented by an {@link domain/entities/commands!IncomingCommand | IncomingCommand}.
3. A newly generated message which has not yet been dispatched is a {@link domain/entities/messages/message!DraftMessage | DraftMessage}.
4. A draft message is sent to the message {@link domain/entities/messages/message!Dispatcher | Dispatcher} which will sent the message to the appropriate room and store it in the room's history as a {@link domain/entities/messages/message!SentMessage | SentMessage}.

The entrypoint to the messaging pipeline is the `MessagesService`. A simplified view of this service looks like this:

```mermaid
stateDiagram
state MessagesService {
direction LR
state if_state <<choice>>
[*] --> if_state
if_state --> IncomingCommand: isCommand
if_state --> IncomingMessage: !isCommand
IncomingMessage --> Dispatcher
Dispatcher --> SentMessage
IncomingCommand --> CommandService
CommandService --> Dispatcher
}
```

## Commands

A message identified as an `IncomingCommand` will be parsed and (if it is a valid command) executed.

```mermaid
stateDiagram
state CommandService {
direction LR
[*] --> IncomingCommand
IncomingCommand --> TokenizedCommand : tokenize
TokenizedCommand --> ParsedCommand : parse
ParsedCommand --> Dispatcher : execute
Dispatcher
}
```

The {@link app/messages/command-service.CommandService | CommandService} applies the following steps to match parsed commands and execute them with the appropriate use case:

- The {@link domain/usecases/commands/parse/tokenize-command.tokenizeCommand | tokenizeCommand} function perfoms a lexing role, tokenizing the incoming commmand based on whitespace and returning a {@link domain/usecases/commands/parse/tokenize-command.TokenizedCommand | TokenizedCommand} which allows for easy parsing.
- The {@link domain/usecases/commands/parse/parse-command.ParseCommandUseCase | ParseCommandUseCase} class parses tokenized commands, returning a {@link domain/usecases/commands/parse/parsed-command.ParsedCommand | ParsedCommand}, a type safe discriminated union of all possible commands.
- Once the command is parsed, the `CommandService` will match the parsed command and execute the corresponding use case.
- These use cases will update entities and send messages via the `Dispatcher` as appropriate.
1 change: 1 addition & 0 deletions services/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@
"ts-node": "^10.0.0",
"tsconfig-paths": "4.2.0",
"typedoc": "0.26.7",
"typedoc-plugin-mermaid": "1.12.0",
"typescript": "5.6.2"
}
}
2 changes: 1 addition & 1 deletion services/api/src/app/auth/auth.module.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Module } from '@nestjs/common';
import { AuthService } from '@usecases/auth.service';
import { AuthService } from '@usecases/auth-service';
import { CaslAuthService } from './casl.auth.service';
import { Auth0Module } from './auth0/auth0.module';

Expand Down
2 changes: 1 addition & 1 deletion services/api/src/app/auth/casl.auth.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AuthorizeParams, AuthService } from '@usecases/auth.service';
import { AuthorizeParams, AuthService } from '@usecases/auth-service';
import { MembershipsRepository } from '@entities/memberships/memberships-repository';
import { ConsoleLogger, Injectable } from '@nestjs/common';
import { defineRolesForUser } from './permissions/roles';
Expand Down
2 changes: 1 addition & 1 deletion services/api/src/app/auth/permissions/roles.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { ContentPolicy, JoinPolicy } from '@entities/rooms/room';
import { RoomFactory } from '@fixtures/messages/room.factory';
import { UserFactory } from '@fixtures/messages/user.factory';
import { defineRolesForUser } from './roles';
import { Role } from '@usecases/auth.service';
import { Role } from '@usecases/auth-service';

describe('defineRolesForUser', () => {
const user = UserFactory.build();
Expand Down
2 changes: 1 addition & 1 deletion services/api/src/app/auth/permissions/roles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
} from '@entities/memberships/membership';
import { ContentPolicy, JoinPolicy } from '@entities/rooms/room';
import { User } from '@entities/users/user';
import { Role } from '@usecases/auth.service';
import { Role } from '@usecases/auth-service';

export const defineRolesForUser = (user: User, memberships: Membership[]) => {
const { can, build } = new AbilityBuilder(createMongoAbility);
Expand Down
2 changes: 1 addition & 1 deletion services/api/src/app/dispatcher/dispatcher.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { MessagesRepository } from '@entities/messages/messages-repository';
import { RoomsRepository } from '@entities/rooms/rooms-repository';
import { User } from '@entities/users/user';
import { ConsoleLogger, Injectable } from '@nestjs/common';
import { AuthService, Role } from '@usecases/auth.service';
import { AuthService, Role } from '@usecases/auth-service';
import { fromEvent, merge, Observable } from 'rxjs';
import { EventEmitter } from 'stream';

Expand Down
2 changes: 1 addition & 1 deletion services/api/src/app/messages/messages.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { AuthModule } from '@app/auth/auth.module';
import { SendMessageUseCase } from '@usecases/messages/send';
import { GetMessagesUseCase } from '@usecases/messages/get-messages';
import { ParseCommandUseCase } from '@usecases/commands/parse/parse-command';
import { CommandService } from '@app/messages/command.service';
import { CommandService } from '@app/messages/command-service';
import { RenameRoomUseCase } from '@usecases/rooms/rename';
import { RenameUserUseCase } from '@usecases/users/rename';
import { HelpCommandUseCase } from '@usecases/commands/help';
Expand Down
2 changes: 1 addition & 1 deletion services/api/src/app/messages/messages.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { RoomFactory } from '@fixtures/messages/room.factory';
import { User } from '@entities/users/user';
import { mock, MockProxy } from 'jest-mock-extended';
import { SendMessageUseCase } from '@usecases/messages/send';
import { CommandService } from './command.service';
import { CommandService } from './command-service';
import { UnauthorizedException } from '@nestjs/common';
import { systemUser } from '@entities/users/system-user';
import { IncomingCommand } from '@entities/commands';
Expand Down
2 changes: 1 addition & 1 deletion services/api/src/app/messages/messages.service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { HttpException, Injectable } from '@nestjs/common';
import { CreateMessageDto } from './dto/messages';
import { SendMessageUseCase } from '@usecases/messages/send';
import { CommandService } from '@app/messages/command.service';
import { CommandService } from '@app/messages/command-service';
import { systemUser } from '@entities/users/system-user';
import { User } from '@entities/users/user';
import { IncomingMessage } from '@entities/messages/message';
Expand Down
4 changes: 0 additions & 4 deletions services/api/src/domain/entities/memberships/membership.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,6 @@ export enum MembershipStatus {
* Record representing the membership status of a user in a room for a specific period.
*
* A user will have a history of zero or more non-overlapping `Membership` records for each room.
*
* There are a number of predicates available to assist with filtering: {@link isCurrent}, {@link isActive}, {@link forRoom}, etc.
*
* There are also convenience functions to determine the membership
*/
export type Membership = {
/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { AppLogger } from '@app/app.logger';
import { TestAuthService } from '@fixtures/auth/test-auth-service';
import { Role } from './auth.service';
import { Role } from './auth-service';
import { UserFactory } from '@fixtures/messages/user.factory';
import { RoomFactory } from '@fixtures/messages/room.factory';
import { UnauthorizedException } from '@nestjs/common';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export type Command = {
parse: CommandParser;
};

type CommandParserParams = {
export type CommandParserParams = {
matchTokens: string[];
schema: CommandSchema;
signature: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { RoomFactory } from '@fixtures/messages/room.factory';
import { UserFactory } from '@fixtures/messages/user.factory';
import { UnauthorizedException } from '@nestjs/common';
import { AppLogger } from '@app/app.logger';
import { Role } from '@usecases/auth.service';
import { Role } from '@usecases/auth-service';
import mock, { MockProxy } from 'jest-mock-extended/lib/Mock';
import { Dispatcher } from '@entities/messages/message';
import { TestUsersRepository } from '@data/repositories/test/test.users.repository';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { RoomsRepository } from '@entities/rooms/rooms-repository';
import { User } from '@entities/users/user';
import { UsersRepository } from '@entities/users/users-repository';
import { Injectable } from '@nestjs/common';
import { AuthService, Role } from '@usecases/auth.service';
import { AuthService, Role } from '@usecases/auth-service';

export type ApproveRequestParams = {
authenticatedUser: User;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { RoomFactory } from '@fixtures/messages/room.factory';
import { UserFactory } from '@fixtures/messages/user.factory';
import { UnauthorizedException } from '@nestjs/common';
import { AppLogger } from '@app/app.logger';
import { Role } from '@usecases/auth.service';
import { Role } from '@usecases/auth-service';
import mock, { MockProxy } from 'jest-mock-extended/lib/Mock';
import { Dispatcher } from '@entities/messages/message';
import { InviteUseCase } from './invite';
Expand Down
2 changes: 1 addition & 1 deletion services/api/src/domain/usecases/memberships/invite.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AuthService, Role } from '@usecases/auth.service';
import { AuthService, Role } from '@usecases/auth-service';
import {
MembershipStatus,
getMembershipStatus,
Expand Down
2 changes: 1 addition & 1 deletion services/api/src/domain/usecases/memberships/join.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { UserFactory } from '@fixtures/messages/user.factory';
import { UnauthorizedException } from '@nestjs/common';
import { JoinRoomUseCase } from './join';
import { AppLogger } from '@app/app.logger';
import { Role } from '@usecases/auth.service';
import { Role } from '@usecases/auth-service';
import mock, { MockProxy } from 'jest-mock-extended/lib/Mock';
import { Dispatcher } from '@entities/messages/message';

Expand Down
2 changes: 1 addition & 1 deletion services/api/src/domain/usecases/memberships/join.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AuthService, Role } from '@usecases/auth.service';
import { AuthService, Role } from '@usecases/auth-service';
import { MembershipStatus } from '@entities/memberships/membership';
import { MembershipsRepository } from '@entities/memberships/memberships-repository';
import { RoomsRepository } from '@entities/rooms/rooms-repository';
Expand Down
2 changes: 1 addition & 1 deletion services/api/src/domain/usecases/memberships/leave.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { TestRoomsRepository } from '@data/repositories/test/test.rooms.reposito
import { RoomFactory } from '@fixtures/messages/room.factory';
import { UserFactory } from '@fixtures/messages/user.factory';
import { AppLogger } from '@app/app.logger';
import { Role } from '@usecases/auth.service';
import { Role } from '@usecases/auth-service';
import mock, { MockProxy } from 'jest-mock-extended/lib/Mock';
import { Dispatcher, UpdatedEntity } from '@entities/messages/message';
import { LeaveRoomUseCase } from './leave';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { TestAuthService } from '@fixtures/auth/test-auth-service';
import { MessageFactory } from '@fixtures/messages/message.factory';
import { UnauthorizedException } from '@nestjs/common';
import { AppLogger } from '@app/app.logger';
import { Role } from '@usecases/auth.service';
import { Role } from '@usecases/auth-service';
import { mock } from 'jest-mock-extended';

describe('GetMessagesUseCase', () => {
Expand Down
2 changes: 1 addition & 1 deletion services/api/src/domain/usecases/messages/get-messages.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Role, AuthService } from '@usecases/auth.service';
import { Role, AuthService } from '@usecases/auth-service';
import { RoomsRepository } from '@entities/rooms/rooms-repository';
import { User } from '@entities/users/user';
import { Injectable } from '@nestjs/common';
Expand Down
2 changes: 1 addition & 1 deletion services/api/src/domain/usecases/messages/send.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { SendMessageUseCase } from './send';
import { Dispatcher, DraftMessage } from '@entities/messages/message';
import { mock, MockProxy } from 'jest-mock-extended';
import { AppLogger } from '@app/app.logger';
import { Role } from '@usecases/auth.service';
import { Role } from '@usecases/auth-service';

describe('SendMessageUseCase', () => {
let sendMessage: SendMessageUseCase;
Expand Down
2 changes: 1 addition & 1 deletion services/api/src/domain/usecases/messages/send.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Dispatcher, DraftMessage } from '@entities/messages/message';
import { RoomsRepository } from '@entities/rooms/rooms-repository';
import { User } from '@entities/users/user';
import { Injectable } from '@nestjs/common';
import { Role, AuthService } from '@usecases/auth.service';
import { Role, AuthService } from '@usecases/auth-service';

@Injectable()
export class SendMessageUseCase {
Expand Down
2 changes: 1 addition & 1 deletion services/api/src/domain/usecases/rooms/about-room.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { RoomFactory } from '@fixtures/messages/room.factory';
import { UserFactory } from '@fixtures/messages/user.factory';
import { UnauthorizedException } from '@nestjs/common';
import { AppLogger } from '@app/app.logger';
import { Role } from '@usecases/auth.service';
import { Role } from '@usecases/auth-service';
import mock, { MockProxy } from 'jest-mock-extended/lib/Mock';
import { Dispatcher } from '@entities/messages/message';
import { AboutRoomUseCase } from './about-room';
Expand Down
2 changes: 1 addition & 1 deletion services/api/src/domain/usecases/rooms/about-room.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { RoomsRepository } from '@entities/rooms/rooms-repository';
import { User } from '@entities/users/user';
import { UsersRepository } from '@entities/users/users-repository';
import { Injectable } from '@nestjs/common';
import { AuthService, Role } from '@usecases/auth.service';
import { AuthService, Role } from '@usecases/auth-service';

export type AboutRoomParams = {
authenticatedUser: User;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { UserFactory } from '@fixtures/messages/user.factory';
import { UnauthorizedException } from '@nestjs/common';
import { Dispatcher } from '@entities/messages/message';
import { AppLogger } from '@app/app.logger';
import { Role } from '@usecases/auth.service';
import { Role } from '@usecases/auth-service';
import { ConfigureRoomUseCase } from './configure-room';
import { ContentPolicy, JoinPolicy } from '@entities/rooms/room';

Expand Down
2 changes: 1 addition & 1 deletion services/api/src/domain/usecases/rooms/configure-room.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { ContentPolicy, JoinPolicy } from '@entities/rooms/room';
import { RoomsRepository } from '@entities/rooms/rooms-repository';
import { User } from '@entities/users/user';
import { Injectable } from '@nestjs/common';
import { AuthService, Role } from '@usecases/auth.service';
import { AuthService, Role } from '@usecases/auth-service';

export type ConfigureRoomParams = {
roomId: string;
Expand Down
2 changes: 1 addition & 1 deletion services/api/src/domain/usecases/rooms/get.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { RoomFactory } from '@fixtures/messages/room.factory';
import { UserFactory } from '@fixtures/messages/user.factory';
import { GetRoomUseCase } from './get';
import { AppLogger } from '@app/app.logger';
import { Role } from '@usecases/auth.service';
import { Role } from '@usecases/auth-service';
import { TestMembershipsRepository } from '@data/repositories/test/test.memberships.repository';
import { MembershipStatus } from '@entities/memberships/membership';

Expand Down
2 changes: 1 addition & 1 deletion services/api/src/domain/usecases/rooms/get.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AuthService, Role } from '@usecases/auth.service';
import { AuthService, Role } from '@usecases/auth-service';
import { Room } from '@entities/rooms/room';
import { RoomsRepository } from '@entities/rooms/rooms-repository';
import { User } from '@entities/users/user';
Expand Down
2 changes: 1 addition & 1 deletion services/api/src/domain/usecases/rooms/rename.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { UserFactory } from '@fixtures/messages/user.factory';
import { UnauthorizedException } from '@nestjs/common';
import { Dispatcher } from '@entities/messages/message';
import { AppLogger } from '@app/app.logger';
import { Role } from '@usecases/auth.service';
import { Role } from '@usecases/auth-service';

describe('RenameRoomUseCase', () => {
let rename: RenameRoomUseCase;
Expand Down
2 changes: 1 addition & 1 deletion services/api/src/domain/usecases/rooms/rename.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AuthService, Role } from '@usecases/auth.service';
import { AuthService, Role } from '@usecases/auth-service';
import { RoomsRepository } from '@entities/rooms/rooms-repository';
import { User } from '@entities/users/user';
import { Injectable } from '@nestjs/common';
Expand Down
2 changes: 1 addition & 1 deletion services/api/src/fixtures/auth/test-auth-service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AuthService, AuthorizeParams } from '@usecases/auth.service';
import { AuthService, AuthorizeParams } from '@usecases/auth-service';
import { equals } from 'rambda';

type Permission = Omit<AuthorizeParams, 'message'>;
Expand Down
4 changes: 4 additions & 0 deletions services/api/tsdoc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json",
"extends": ["typedoc-plugin-mermaid/tsdoc.json"]
}
Loading

0 comments on commit 6eb7a13

Please sign in to comment.