Documentation
¶
Overview ¶
Package domain implements the business logic for working with boards, users and invites:
- functions to create, validate and work with instances of boards/users/invites
- general data store interface BoardDataStore, which makes it easy to switch out the actual storage mechanism for the data
- BoardService implements operations that can be performed on boards, users and invites
Index ¶
- type Board
- type BoardCreated
- type BoardDataStore
- type BoardDeleted
- type BoardEdit
- type BoardInvite
- type BoardService
- func (bs *BoardService) AcceptInvite(ctx context.Context, boardId string, inviteId string, user User) error
- func (bs *BoardService) CreateBoard(ctx context.Context, name, description string, user User) (BoardWithUsersAndInvites, error)
- func (bs *BoardService) CreateInvite(ctx context.Context, boardId string, role string, forUser User, fromUser User) (BoardInvite, error)
- func (bs *BoardService) DeclineInvite(ctx context.Context, boardId string, inviteId string, user User) error
- func (bs *BoardService) DeleteBoard(ctx context.Context, boardId string, user User) error
- func (bs *BoardService) DeleteInvite(ctx context.Context, boardId string, inviteId string) error
- func (bs *BoardService) EditBoard(ctx context.Context, boardId string, be BoardEdit, user User) (Board, error)
- func (bs *BoardService) EditBoardUser(ctx context.Context, boardId string, userId string, bue BoardUserEdit, ...) (BoardUser, error)
- func (bs *BoardService) RemoveUser(ctx context.Context, boardId string, userId string) error
- type BoardUser
- type BoardUserEdit
- type BoardWithUsersAndInvites
- func (b BoardWithUsersAndInvites) ContainsInvite(inviteId string) bool
- func (b BoardWithUsersAndInvites) ContainsInviteForUser(userId string) bool
- func (b BoardWithUsersAndInvites) ContainsUser(userId string) bool
- func (b BoardWithUsersAndInvites) Invite(inviteId string) (BoardInvite, bool)
- func (b BoardWithUsersAndInvites) InviteCount() int
- func (b BoardWithUsersAndInvites) InviteForUser(userId string) (BoardInvite, bool)
- func (b BoardWithUsersAndInvites) User(userId string) (BoardUser, bool)
- func (b BoardWithUsersAndInvites) UserCount() int
- type DatastoreBoardUpdate
- func (u *DatastoreBoardUpdate) IsEmpty() bool
- func (u *DatastoreBoardUpdate) RemoveInvite(inviteId string) *DatastoreBoardUpdate
- func (u *DatastoreBoardUpdate) RemoveUser(userId string) *DatastoreBoardUpdate
- func (u *DatastoreBoardUpdate) UpdateInvite(invite BoardInvite) *DatastoreBoardUpdate
- func (u *DatastoreBoardUpdate) UpdateUser(user BoardUser) *DatastoreBoardUpdate
- func (u *DatastoreBoardUpdate) WithBoard(b Board) *DatastoreBoardUpdate
- type EventPublisher
- type QueryParams
- type TransactionExpectation
- type User
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Board ¶
type Board struct { // Random UUIDv4 with prefix "b-" BoardId string Name string Description string // Time the board was created as Unix time (nanoseconds) CreatedTime int64 // The user that created this board. // Note that the User value contains the id and name of the user. // While the id should be unique and never change, a user usually has the ability to change their name. // The name of the user kept with this board would then no longer be up to date. // If we don't want to accept these inconsistencies, we would either have to retrieve the name for a given user id separately or // update the name here every time it changes. CreatedBy User // Last time the board was modified (e.g. the name or description changed) as Unix time (nanoseconds) ModifiedTime int64 // User that performed the latest modification ModifiedBy User }
func (*Board) IsDescriptionValid ¶
A board description can be at most descriptionMaxLength bytes long.
func (*Board) IsNameValid ¶
A board name is valid if it is not empty and not longer than nameMaxLength bytes.
type BoardCreated ¶
type BoardDataStore ¶
type BoardDataStore interface { // Update a single board and/or some users/invites (belonging to a single board). // // To implement optimistic concurrency, this method takes a TransactionExpectation (as part of the update parameter). // The TransactionExpectation represents the last time the board was modified. // Any implementation of this interface should guarantee that the board (and/or its users/invites) is updated only // if the board wasn't changed (i.e. it is still in the same state represented by the TransactionExpectation). UpdateBoard(ctx context.Context, boardId string, update *DatastoreBoardUpdate) error DeleteBoard(ctx context.Context, boardId string) error // Returns the board with the given id together with all its users and invites. // There are (almost) no separate methods to query or retrieve specific users/invites. // This is feasible since the number of users and invites per board is limited. // If in the future we wanted to remove these limits, it would make sense to treat boards, board users and board invites // as separate entities, with their own data store methods to retrieve and modify them. // // This method returns a TransactionExpectation that can later be passed back into the UpdateBoard method, // to make sure that the board (and its users/invites) wasn't changed before it is updated. Board(ctx context.Context, boardId string) (BoardWithUsersAndInvites, TransactionExpectation, error) Boards(ctx context.Context, boardIds []string) ([]Board, error) BoardsForUser(ctx context.Context, userId string, qp QueryParams) ([]Board, error) User(ctx context.Context, boardId string, userId string) (BoardUser, error) InvitesForUser(ctx context.Context, userId string, qp QueryParams) (map[string]BoardInvite, error) }
Any type implementing this interface can be used as the data store for boards, users and invites. We consider a board together with its users and invites as the unit of consistency, they can be retrieved and modified together. To avoid inconsistencies when multiple concurrent requests try to modify a board (and/or its users/invites), we use optimistic transactions/concurrency control (see also the comments on the TransactionExpectation interface and the Board() and UpdateBoard() methods).
type BoardDeleted ¶
type BoardInvite ¶
type BoardInvite struct { // Random UUIDv4 with prefix "i-" InviteId string // Role a user that accepts the invite would have on the board. Role string // If not empty, only the given user can accept the invite. // If empty, any user can. User User // Time the invite was created as Unix time (nanoseconds). CreatedTime int64 // User that created the invite. CreatedBy User // Time the invite expires as Unix time (nanoseconds). // After this time the invite can no longer be accepted. ExpiresTime int64 }
An invite to join a board. Can be either a general invite, that can be accepted by any user or an invite specific to a user.
func NewBoardInvite ¶
func (BoardInvite) IsExpired ¶
func (i BoardInvite) IsExpired() bool
Returns true if the invite has expired.
type BoardService ¶
type BoardService struct {
// contains filtered or unexported fields
}
BoardService implements operations on boards, users and invites. It uses an implementation of BoardDataStore to read and persist boards/users/invites, and an implementation of EventPublisher to make events available to other components/systems. BoardService does not perform authorization, that should be handled in application services using this package.
We consider a board together with its users and invites as the unit of consistency, i.e. operations on BoardService can run concurrently only for different boards. E.g. if two operations try to concurrently create a new invite for the same user on the same board, at most one of the operations should succeed, since for a board there should be at most one invite for a user at any given time. BoardService guarantees consistency by using the optimistic transaction capabilities required of any BoardDataStore implementation.
BoardService handles transactions explicitly, it tells BoardDataStore what data it expects to have not been changed. Instead it could also be implemented implicitly. For operations that read data, BoardDataStore could store transaction expectations in the context value (see the "session" package for a possible implementation), and read/verify those expecatations when modifying data. This works because in methods of BoardService we always pass the same context value to invocations of BoardDataStore methods. With this approach we could change the BoardDataStore interface to no longer return/accept TransactionExpectation values. A drawback of the implicit approach is that it might not be as obvious or easy to understand for someone reading the BoardService code, what kind of consistency is guaranteed/what data is changed transactionally. Note that both the explicit and implicit approach described here still implement optmistic transactions, the only difference is where the transaction expectations are created/kept. For the given application the number of users of a board is bounded, which in practice makes it very unlikely that concurrent operations that could lead to inconsistencies even happen that often.
Another way of implementing transactions would be to pass an update function to the data store, e.g. func(oldValue Board) (newValue Board) { ... }. I.e. instead of changing/modifying the data in the service method, the service method would pass a function of this type to the datastore. The datastore method would then read the old value, pass it to the function to get the modified value and then persist it. Since all of this happens in the data store implementation, it has complete control to guarantee consistency using a transaction, no need to pass TransactionExpectation back and forth.
func NewBoardService ¶
func NewBoardService(ds BoardDataStore, ep EventPublisher) *BoardService
The BoardDataStore passed must not be nil. The EventPublisher can be, in which case the events will just end up nowhere.
func (*BoardService) AcceptInvite ¶
func (*BoardService) CreateBoard ¶
func (bs *BoardService) CreateBoard(ctx context.Context, name, description string, user User) (BoardWithUsersAndInvites, error)
func (*BoardService) CreateInvite ¶
func (bs *BoardService) CreateInvite(ctx context.Context, boardId string, role string, forUser User, fromUser User) (BoardInvite, error)
func (*BoardService) DeclineInvite ¶
func (bs *BoardService) DeclineInvite(ctx context.Context, boardId string, inviteId string, user User) error
Only invites for a specific user can be declined.
func (*BoardService) DeleteBoard ¶
func (*BoardService) DeleteInvite ¶
func (*BoardService) EditBoardUser ¶
func (bs *BoardService) EditBoardUser(ctx context.Context, boardId string, userId string, bue BoardUserEdit, user User) (BoardUser, error)
func (*BoardService) RemoveUser ¶
type BoardUser ¶
type BoardUser struct { User User // Role the user has for the board, see the auth package for available roles. // Determines what the user can do on the board. Role string // Time the user joined the board as Unix time (nanoseconds). CreatedTime int64 // The user that invited this user to the board. InvitedBy User // Last time this BoardUser was modified (e.g. the role changed) as Unix time (nanoseconds). ModifiedTime int64 ModifiedBy User }
type BoardUserEdit ¶
type BoardWithUsersAndInvites ¶
type BoardWithUsersAndInvites struct { Board Board Users []BoardUser Invites []BoardInvite }
A board with all its users and invites, which we consider the unit of consistency. I.e. the service methods supported by the datastore should guarantee that there are no concurrent operations on a single board (including its users and invites).
This struct does not provide methods to manipulate the sets of users and invites, instead the updates should be encoded using a DatastoreBoardUpdate value.
Note also that since the number of users and invites for a single board is limited, we do not have to worry about the implications of loading all users/invites for performance and memory.
func (BoardWithUsersAndInvites) ContainsInvite ¶
func (b BoardWithUsersAndInvites) ContainsInvite(inviteId string) bool
func (BoardWithUsersAndInvites) ContainsInviteForUser ¶
func (b BoardWithUsersAndInvites) ContainsInviteForUser(userId string) bool
func (BoardWithUsersAndInvites) ContainsUser ¶
func (b BoardWithUsersAndInvites) ContainsUser(userId string) bool
func (BoardWithUsersAndInvites) Invite ¶
func (b BoardWithUsersAndInvites) Invite(inviteId string) (BoardInvite, bool)
func (BoardWithUsersAndInvites) InviteCount ¶
func (b BoardWithUsersAndInvites) InviteCount() int
func (BoardWithUsersAndInvites) InviteForUser ¶
func (b BoardWithUsersAndInvites) InviteForUser(userId string) (BoardInvite, bool)
func (BoardWithUsersAndInvites) User ¶
func (b BoardWithUsersAndInvites) User(userId string) (BoardUser, bool)
func (BoardWithUsersAndInvites) UserCount ¶
func (b BoardWithUsersAndInvites) UserCount() int
type DatastoreBoardUpdate ¶
type DatastoreBoardUpdate struct { TransactionExpecation TransactionExpectation // Set to true if the board should be updated with the value defined by the "Board" field. UpdateBoard bool Board Board UpdateUsers []BoardUser // User ids of users that should be removed RemoveUsers []string UpdateInvites []BoardInvite // Invite ids of invites that should be removed RemoveInvites []string }
Defines an update to a single board and its users and/or invites.
The set of changes to the users/invites are defined using update and remove lists, instead of just passing the new complete lists of users/invites. This might be useful for data store implementations that keep users and invites separately from their board, e.g. to optimize or make it easier to perform certain queries.
func NewDatastoreBoardUpdate ¶
func NewDatastoreBoardUpdate(te TransactionExpectation) *DatastoreBoardUpdate
func (*DatastoreBoardUpdate) IsEmpty ¶
func (u *DatastoreBoardUpdate) IsEmpty() bool
func (*DatastoreBoardUpdate) RemoveInvite ¶
func (u *DatastoreBoardUpdate) RemoveInvite(inviteId string) *DatastoreBoardUpdate
func (*DatastoreBoardUpdate) RemoveUser ¶
func (u *DatastoreBoardUpdate) RemoveUser(userId string) *DatastoreBoardUpdate
func (*DatastoreBoardUpdate) UpdateInvite ¶
func (u *DatastoreBoardUpdate) UpdateInvite(invite BoardInvite) *DatastoreBoardUpdate
func (*DatastoreBoardUpdate) UpdateUser ¶
func (u *DatastoreBoardUpdate) UpdateUser(user BoardUser) *DatastoreBoardUpdate
func (*DatastoreBoardUpdate) WithBoard ¶
func (u *DatastoreBoardUpdate) WithBoard(b Board) *DatastoreBoardUpdate
type EventPublisher ¶
Any type implementing this interface can be used by BoardService to publish events. There are many possible types of adaptors for this interface:
- An event "broker" local to this application, that takes these events and makes them available for other modules to listen to, e.g. a notification module.
- Publish the events to an external system like Apache Kafka or a cloud service like Google Pub/Sub.
type QueryParams ¶
type QueryParams struct { // Valid limit values are between 1 and 100 (inclusive) Limit int // Cursor for pagination, only return elements that were created at or before the given unix time (nanoseconds). // To obtain the cursor value, one can use the created time of the oldest element in the last query and subtract 1. // Note that this could skip some elements, if they were created at the exact same time, however this is very unlikely // since we use unix time in nanoseconds. Cursor int64 }
Parameters for data store queries for boards and invites. Can be used to limit the number of results returned or provide a cursor for pagination. By default, elements will be sorted from newest to oldest (i.e. by descending created time).
func NewQueryParams ¶
func NewQueryParams() QueryParams
func (QueryParams) WithCursor ¶
func (qp QueryParams) WithCursor(cursor int64) QueryParams
func (QueryParams) WithLimit ¶
func (qp QueryParams) WithLimit(limit int) QueryParams
type TransactionExpectation ¶
type TransactionExpectation interface{}
A TransactionExpectation should contain information about when some piece of data was last modified. It is used to implement optimistic transactions/concurrency control.
In particular for BoardDataStore, TransactionExpectation can be used to guarantee consistency in the following way:
- We can retrieve a board (with all the users and invites) using the Board() method.
- Board() also returns a value implementing TransactionExpectation, it will typically contain the last time the board (and/or its users/invites) was modified.
- When we want to update/modify the board (and/or its users/invites), we can pass the TransactionExpectation back to the UpdateBoard() method (as part of the "update" parameter).
- A correct data store implementation will have to guarantee that the update/modification is only performed if the board (and/or users/invites) was not modified since the time defined in the TransactionExpectation value.
Right now, we will only ever need to pass a single TransactionExpectation. However for a more complicated scenario, the TransactionExpectation interface could require a method "Combine(other TransactionExpectation) TransactionExpectation" that can be used to combine multiple expectations. I.e. the combined expectation would be used to represent the last modification time of multiple pieces of data.
type User ¶
User represents the information about a user we need in this context. A User value is used in other types to e.g. represent the user that created a board or an invite.
We assume that users/authentication are managed elsewhere, e.g. another package or an outside system like Firebase Authentication. There a user might have more attributes like an email address, a photo, or birth date.
While the id of a user shouldn't change, the user can possibly change their name in the authentication system that manages users.