From fcc774390d9f934d60b80aebb7ef3056ecc4b038 Mon Sep 17 00:00:00 2001 From: Pat Garrity Date: Thu, 8 Aug 2024 22:20:56 -0500 Subject: [PATCH] some changes to initial scope (no users at all) and db updates and sql --- modules/db/src/main/resources/sql/1.sql | 50 +++++++++ .../main/scala/gs/smolban/db/GroupDb.scala | 11 ++ .../src/main/scala/gs/smolban/db/TagDb.scala | 9 ++ .../scala/gs/smolban/model/CreatedBy.scala | 24 ---- .../main/scala/gs/smolban/model/Group.scala | 5 +- .../src/main/scala/gs/smolban/model/Tag.scala | 57 ++++++++-- .../main/scala/gs/smolban/model/Ticket.scala | 9 +- .../scala/gs/smolban/model/users/Role.scala | 104 ------------------ .../scala/gs/smolban/model/users/Scope.scala | 78 ------------- .../scala/gs/smolban/model/users/User.scala | 96 ---------------- .../gs/smolban/model/users/Username.scala | 47 -------- 11 files changed, 121 insertions(+), 369 deletions(-) create mode 100644 modules/db/src/main/resources/sql/1.sql create mode 100644 modules/db/src/main/scala/gs/smolban/db/GroupDb.scala create mode 100644 modules/db/src/main/scala/gs/smolban/db/TagDb.scala delete mode 100644 modules/model/src/main/scala/gs/smolban/model/CreatedBy.scala delete mode 100644 modules/model/src/main/scala/gs/smolban/model/users/Role.scala delete mode 100644 modules/model/src/main/scala/gs/smolban/model/users/Scope.scala delete mode 100644 modules/model/src/main/scala/gs/smolban/model/users/User.scala delete mode 100644 modules/model/src/main/scala/gs/smolban/model/users/Username.scala diff --git a/modules/db/src/main/resources/sql/1.sql b/modules/db/src/main/resources/sql/1.sql new file mode 100644 index 0000000..2488718 --- /dev/null +++ b/modules/db/src/main/resources/sql/1.sql @@ -0,0 +1,50 @@ +-- sqlite3 + +CREATE TABLE IF NOT EXISTS groups( + id BIGINT PRIMARY KEY, + group_id BLOB NOT NULL, + slug TEXT NOT NULL, + created_at DATETIME NOT NULL +); + +CREATE UNIQUE INDEX IF NOT EXISTS idx_groups_group_id ON groups(group_id); + +CREATE TABLE IF NOT EXISTS tags( + id BIGINT PRIMARY KEY, + value TEXT NOT NULL, + created_at DATETIME NOT NULL +); + +CREATE UNIQUE INDEX IF NOT EXISTS idx_tags_value ON tags(value); + +CREATE TABLE IF NOT EXISTS tickets( + id BIGINT PRIMARY KEY, + ticket_number INTEGER NOT NULL, + group_id BLOB NOT NULL, + created_at DATETIME NOT NULL, + title TEXT NOT NULL, + description TEXT NOT NULL, + status TEXT NOT NULL +); + +CREATE UNIQUE INDEX IF NOT EXISTS idx_tickets_number_group +ON tickets(group_id, ticket_number); + +CREATE TABLE IF NOT EXISTS ticket_tags( + id BIGINT PRIMARY KEY, + ticket_id BIGINT NOT NULL, + tag_id BIGINT NOT NULL +); + +CREATE UNIQUE INDEX IF NOT EXISTS idx_ticket_tags_ticket_tag +ON ticket_tags(ticket_id, tag_id); + +CREATE TABLE IF NOT EXISTS ticket_history( + id BIGINT PRIMARY KEY, + ticket_id BIGINT NOT NULL, + status TEXT NOT NULL, + set_at DATETIME NOT NULL +); + +CREATE INDEX IF NOT EXISTS idx_ticket_history_order_ticket_set_at +ON ticket_history(ticket_id, set_at); diff --git a/modules/db/src/main/scala/gs/smolban/db/GroupDb.scala b/modules/db/src/main/scala/gs/smolban/db/GroupDb.scala new file mode 100644 index 0000000..4b95826 --- /dev/null +++ b/modules/db/src/main/scala/gs/smolban/db/GroupDb.scala @@ -0,0 +1,11 @@ +package gs.smolban.db + +import cats.data.EitherT +import gs.smolban.model.Group + +trait GroupDb[F[_]]: + def createGroup(group: Group): EitherT[F, DbError, Group] + + def readGroup(id: Group.Id): F[Option[Group]] + + def deleteGroup(id: Group.Id): F[Boolean] diff --git a/modules/db/src/main/scala/gs/smolban/db/TagDb.scala b/modules/db/src/main/scala/gs/smolban/db/TagDb.scala new file mode 100644 index 0000000..263d293 --- /dev/null +++ b/modules/db/src/main/scala/gs/smolban/db/TagDb.scala @@ -0,0 +1,9 @@ +package gs.smolban.db + +import cats.data.EitherT +import gs.smolban.model.Tag + +trait TagDb[F[_]]: + def createTag(tag: Tag.Value): EitherT[F, DbError, Tag] + def readTag(id: Tag.Id): F[Option[Tag]] + def deleteTag(id: Tag.Id): F[Boolean] diff --git a/modules/model/src/main/scala/gs/smolban/model/CreatedBy.scala b/modules/model/src/main/scala/gs/smolban/model/CreatedBy.scala deleted file mode 100644 index 6e4f27e..0000000 --- a/modules/model/src/main/scala/gs/smolban/model/CreatedBy.scala +++ /dev/null @@ -1,24 +0,0 @@ -package gs.smolban.model - -import cats.Show -import gs.smolban.model.users.User - -opaque type CreatedBy = User.Id - -object CreatedBy: - - def apply(timestamp: User.Id): CreatedBy = timestamp - - given CanEqual[CreatedBy, CreatedBy] = CanEqual.derived - - given Show[CreatedBy] = _.toUserId().toUUID().withoutDashes() - - extension (createdAt: CreatedBy) - /** Unwrap this value. - * - * @return - * The underlying `User.Id` value. - */ - def toUserId(): User.Id = createdAt - -end CreatedBy diff --git a/modules/model/src/main/scala/gs/smolban/model/Group.scala b/modules/model/src/main/scala/gs/smolban/model/Group.scala index e9fa2aa..31994b6 100644 --- a/modules/model/src/main/scala/gs/smolban/model/Group.scala +++ b/modules/model/src/main/scala/gs/smolban/model/Group.scala @@ -13,14 +13,11 @@ import gs.uuid.v0.UUID * The unique slug for the group. * @param createdAt * The instant at which this group was created. - * @param createdBy - * The unique identifier of the user who created this group. */ case class Group( id: Group.Id, slug: Slug, - createdAt: CreatedAt, - createdBy: CreatedBy + createdAt: CreatedAt ) object Group: diff --git a/modules/model/src/main/scala/gs/smolban/model/Tag.scala b/modules/model/src/main/scala/gs/smolban/model/Tag.scala index fbc224d..3d5d49f 100644 --- a/modules/model/src/main/scala/gs/smolban/model/Tag.scala +++ b/modules/model/src/main/scala/gs/smolban/model/Tag.scala @@ -1,21 +1,62 @@ package gs.smolban.model import cats.Show +import java.time.Instant -/** Opaque type for a String that represents a unique `Tag` in Smolban. Tags are - * just arbitrary non-empty strings which can be used to annotate [[Ticket]]. +/** Represents a smolban "tag", or arbitrary string descriptor. * - * Tags are defined _globally_ in Smolban. + * @param id + * The unique ID for this tag. + * @param value + * The unique value of this tag. + * @param createdAt + * The instant this tag was created. */ -opaque type Tag = String // TODO: Make this have a unique id (long?) and value +case class Tag( + id: Tag.Id, + value: Tag.Value, + createdAt: Instant +) object Tag: - def validate(candidate: String): Option[Tag] = - if candidate.isEmpty() then None else Some(candidate) - given CanEqual[Tag, Tag] = CanEqual.derived - given Show[Tag] = t => t + /** Opaque type which represents a [[Tag]] unique identifier. This is a long + * integer. + */ + opaque type Id = Long + + object Id: + def apply(id: Long): Id = id + + given CanEqual[Id, Id] = CanEqual.derived + + given Show[Id] = t => t.toString() + + end Id + + /** Opaque type which represents a [[Tag]] value. These values are non-empty, + * arbitrary strings. + */ + opaque type Value = String + + object Value: + + /** Validate the candidate string - Tag Values must be non-empty. + * + * @param candidate + * The candidate string. + * @return + * The valid value, or `None` if an empty string was given. + */ + def validate(candidate: String): Option[Value] = + if candidate.isEmpty() then None else Some(candidate) + + given CanEqual[Value, Value] = CanEqual.derived + + given Show[Value] = t => t + + end Value end Tag diff --git a/modules/model/src/main/scala/gs/smolban/model/Ticket.scala b/modules/model/src/main/scala/gs/smolban/model/Ticket.scala index 87b95e1..0a9b85f 100644 --- a/modules/model/src/main/scala/gs/smolban/model/Ticket.scala +++ b/modules/model/src/main/scala/gs/smolban/model/Ticket.scala @@ -1,7 +1,6 @@ package gs.smolban.model import cats.Show -import gs.smolban.model.users.User import java.time.Instant /** Tickets represent some tracked work. @@ -12,8 +11,6 @@ import java.time.Instant * Unique identifier of the [[Group]] that owns this ticket. * @param createdAt * The instant at which this ticket was created. - * @param createdBy - * The unique identifier of the [[User]] who created this ticket. * @param title * Arbitrary string title of the ticket. * @param description @@ -24,20 +21,16 @@ import java.time.Instant * Current [[Ticket.Status]] of this ticket. * @param statusHistory * Linear history of this ticket in terms of status changes. - * @param assignee - * If set, this ticket is assigned to a specific user. */ case class Ticket( id: Ticket.Id, group: Group.Id, createdAt: CreatedAt, - createdBy: CreatedBy, title: String, description: String, tags: List[Tag], status: Ticket.Status, - statusHistory: List[(Ticket.Status, Instant)], - assignee: Option[User.Id] + statusHistory: List[(Ticket.Status, Instant)] ): lazy val reference: Ticket.Reference = Ticket.Reference(id, group) diff --git a/modules/model/src/main/scala/gs/smolban/model/users/Role.scala b/modules/model/src/main/scala/gs/smolban/model/users/Role.scala deleted file mode 100644 index afb9699..0000000 --- a/modules/model/src/main/scala/gs/smolban/model/users/Role.scala +++ /dev/null @@ -1,104 +0,0 @@ -package gs.smolban.model.users - -/** Roles define what each [[User]] is allowed to do. - * - * @param name - * The name of the role. - * @param scope - * The [[Scope]] to which the role applies. - */ -sealed abstract class Role( - val name: String, - val scope: Scope -): - - override def toString(): String = - s"[$name]${Role.Delimiter}$scope" - -object Role: - - val Delimiter: String = ":" - - /** Administrator for the API. Can perform any operation through the API. - */ - case object ApiAdmin extends Role(s"api${Delimiter}admin", Scope.ApiGlobal) - - /** Global read access to the API. - */ - case object ApiGlobalReader - extends Role(s"api${Delimiter}reader", Scope.ApiGlobal) - - /** Global write access to the API. - */ - case object ApiGlobalWriter - extends Role(s"api${Delimiter}writer", Scope.ApiGlobal) - - /** Administrator for a specific [[gs.smolban.model.Group]] via the API. Can - * perform any operation through the API for the indicated group. - * - * @param groupId - * The unique identifier for the group to which this role is scoped. - */ - case class ApiGroupAdmin( - groupId: gs.smolban.model.Group.Id - ) extends Role(s"api${Delimiter}group_admin", Scope.ApiGroup(groupId)) - - /** Grants API read access for a specific [[gs.smolban.model.Group]]. - * - * @param groupId - * The unique identifier for the group to which this role is scoped. - */ - case class ApiGroupReader( - groupId: gs.smolban.model.Group.Id - ) extends Role(s"api${Delimiter}group_reader", Scope.ApiGroup(groupId)) - - /** Grants API write access for a specific [[gs.smolban.model.Group]]. - * - * @param groupId - * The unique identifier for the group to which this role is scoped. - */ - case class ApiGroupWriter( - groupId: gs.smolban.model.Group.Id - ) extends Role(s"api${Delimiter}group_writer", Scope.ApiGroup(groupId)) - - /** Administrator for the UI. Can perform any operation through the UI. - */ - case object UiAdmin extends Role(s"ui${Delimiter}admin", Scope.UiGlobal) - - /** Global read access to the UI. - */ - case object UiGlobalReader extends Role("ui:reader", Scope.UiGlobal) - - /** Global write access to the UI. - */ - case object UiGlobalWriter extends Role("ui:writer", Scope.UiGlobal) - - /** Administrator for a specific [[gs.smolban.model.Group]] via the UI. Can - * perform any operation through the UI for the indicated group. - * - * @param groupId - * The unique identifier for the group to which this role is scoped. - */ - case class UiGroupAdmin( - groupId: gs.smolban.model.Group.Id - ) extends Role(s"ui${Delimiter}group_admin", Scope.UiGroup(groupId)) - - /** Grants UI read access for a specific [[gs.smolban.model.Group]]. - * - * @param groupId - * The unique identifier for the group to which this role is scoped. - */ - case class UiGroupReader( - groupId: gs.smolban.model.Group.Id - ) extends Role(s"ui${Delimiter}group_reader", Scope.UiGroup(groupId)) - - /** Grants UI write access for a specific [[gs.smolban.model.Group]]. - * - * @param groupId - * The unique identifier for the group to which this role is scoped. - */ - case class UiGroupWriter( - groupId: gs.smolban.model.Group.Id - ) extends Role(s"ui${Delimiter}group_writer", Scope.UiGroup(groupId)) - -end Role diff --git a/modules/model/src/main/scala/gs/smolban/model/users/Scope.scala b/modules/model/src/main/scala/gs/smolban/model/users/Scope.scala deleted file mode 100644 index 3ff5bc9..0000000 --- a/modules/model/src/main/scala/gs/smolban/model/users/Scope.scala +++ /dev/null @@ -1,78 +0,0 @@ -package gs.smolban.model.users - -import cats.syntax.all.* - -/** Describes a _Scope_, or service of Smolban, to which a user can have access. - * - * Scopes consist of two parts: - * - * - `service`: The name of a service Smolban offers. - * - `name`: The name of the scope within the given service. - * - * @param service - * The name of a service Smolban offers (e.g. `api`, `ui`) - * @param name - * The name of the scope within the service. - */ -sealed abstract class Scope( - val service: String, - val name: String -) - -object Scope: - - given CanEqual[Scope, Scope] = CanEqual.derived - - object Services: - - val Api: String = "api" - val Ui: String = "ui" - - end Services - - object Names: - - val Global: String = "global" - val Group: String = "group" - - end Names - - val Delimiter: String = ":" - - /** The `api:global` scope covers the Smolban API across all groups. - */ - case object ApiGlobal extends Scope(Services.Api, Names.Global): - override def toString(): String = s"[$service$Delimiter$name]" - - /** The `api:group:` scope refers the the Smolban API for a specific - * [[Group]]. - * - * @param groupId - * The unique identifier of the [[Group]]. - */ - case class ApiGroup( - groupId: gs.smolban.model.Group.Id - ) extends Scope(Services.Api, Names.Group): - - override def toString(): String = - s"[$service$Delimiter$name$Delimiter${groupId.show}]" - - /** The `ui:global` scope covers the Smolban UI across all groups. - */ - case object UiGlobal extends Scope(Services.Ui, Names.Global): - override def toString(): String = s"[$service$Delimiter$name]" - - /** The `ui:group:` scope refers the the Smolban UI for a specific - * [[Group]]. - * - * @param groupId - * The unique identifier of the [[Group]]. - */ - case class UiGroup( - groupId: gs.smolban.model.Group.Id - ) extends Scope(Services.Ui, Names.Group): - - override def toString(): String = - s"[$service$Delimiter$name$Delimiter${groupId.show}]" - -end Scope diff --git a/modules/model/src/main/scala/gs/smolban/model/users/User.scala b/modules/model/src/main/scala/gs/smolban/model/users/User.scala deleted file mode 100644 index 1c56f8e..0000000 --- a/modules/model/src/main/scala/gs/smolban/model/users/User.scala +++ /dev/null @@ -1,96 +0,0 @@ -package gs.smolban.model.users - -import cats.Show -import gs.smolban.model.CreatedAt -import gs.uuid.v0.UUID - -/** Represents a user that can interact with Smolban. - * - * @param id - * Unique identifier of this user. - * @param createdAt - * The instant at which this user was created. - * @param username - * The (unique) [[Username]] of this user. - * @param designation - * The [[User.Designation]] of this user. - * @param roles - * List of [[Role]] assigned to this user. - * @param status - * The current [[User.Status]] of this user. - */ -case class User( - id: User.Id, - createdAt: CreatedAt, - username: Username, - designation: User.Designation, - roles: List[Role], - status: User.Status -) - -object User: - - given CanEqual[User, User] = CanEqual.derived - - /** Unique identifier for a [[User]]. This is an opaque type for a UUID. - */ - opaque type Id = UUID - - object Id: - - /** Instantiate a new [[User.Id]]. - * - * @param id - * The underlying UUID. - * @return - * The new [[User.Id]] instance. - */ - def apply(id: UUID): Id = id - - given CanEqual[Id, Id] = CanEqual.derived - - given Show[Id] = _.toUUID().withoutDashes() - - extension (id: Id) - /** Unwrap this User ID. - * - * @return - * The underlying UUID value. - */ - def toUUID(): UUID = id - - end Id - - /** Enumeration that describes the designation of a [[User]]. - */ - sealed abstract class Designation(val name: String) - - object Designation: - - /** Regular users are typically human, and are expected to be using the Web - * UI and _possibly_ APIs. - */ - case object Regular extends Designation("regular") - - /** Service users are intended for use by API-consuming services. They are - * not suitable for human/UI interactive use. - */ - case object Service extends Designation("service") - - end Designation - - /** Enumeration that describes the status of a [[User]] in Smolban. - */ - sealed abstract class Status(val name: String) - - object Status: - - given CanEqual[Status, Status] = CanEqual.derived - - given Show[Status] = _.name - - case object Active extends Status("active") - case object Suspended extends Status("suspended") - case object Off extends Status("off") - -end User diff --git a/modules/model/src/main/scala/gs/smolban/model/users/Username.scala b/modules/model/src/main/scala/gs/smolban/model/users/Username.scala deleted file mode 100644 index a14b137..0000000 --- a/modules/model/src/main/scala/gs/smolban/model/users/Username.scala +++ /dev/null @@ -1,47 +0,0 @@ -package gs.smolban.model.users - -import cats.Show - -/** Opaque type for String that represents a unique username in Smolban. - */ -opaque type Username = String - -object Username: - - /** In Smolban, a [[Username]] must be at most 32 characters long. - */ - val MaximumLength: Int = 32 - - /** In Smolban, a [[Username]] must be at least 3 characters long. - */ - val MinimumLength: Int = 3 - - given CanEqual[Username, Username] = CanEqual.derived - - /** Validate some candidate string, producing a [[Username]] if valid. Smolban - * usernames must be: - * - * - At least 3 characters long. - * - At most 32 characters long. - * - Non-blank -- non-whitespace characters must be used. - * - * @param candidate - * The candidate string to evaluate. - * @return - * The [[Username]], or `None` if the candidate was invalid. - */ - def validate(candidate: String): Option[Username] = - if isValid(candidate) then Some(candidate) else None - - private def isValid(candidate: String): Boolean = - isValidSize(candidate) && isNonBlank(candidate) - - private def isValidSize(candidate: String): Boolean = - candidate.length() >= MinimumLength && candidate.length() <= MaximumLength - - private def isNonBlank(candidate: String): Boolean = - !candidate.isBlank() - - given Show[Username] = u => u - -end Username