Expanding the model and updating documentation.

This commit is contained in:
Pat Garrity 2023-11-28 20:50:53 -06:00
parent e172d782b3
commit ca2a85c81b
Signed by: pfm
GPG key ID: 5CA5D21BAB7F3A76
7 changed files with 152 additions and 39 deletions

View file

@ -32,17 +32,17 @@ Comments may only include text.
## Relational Data Model
- [Table: `content`](#table-content)
- [Table: `posts`](#table-posts)
- [Table: `comments`](#table-comments)
- [Table: `assets`](#table-assets)
- [Table: `tags`](#table-tags)
- [Table: `content_tags`](#table-content_tags)
- [Table: `post_tags`](#table-post_tags)
- [Table: `asset_tags`](#table-asset_tags)
### Table: `content`
### Table: `posts`
```sql
CREATE TABLE content(
CREATE TABLE posts(
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
external_id UUID NOT NULL UNIQUE,
created_at TIMESTAMPTZ NOT NULL,
@ -58,16 +58,18 @@ CREATE TABLE content(
CREATE TABLE comments(
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
external_id UUID NOT NULL UNIQUE,
post_id BIGINT NOT NULL,
created_at TIMESTAMPTZ NOT NULL,
created_by BIGINT NOT NULL,
content TEXT NOT NULL,
contents TEXT NOT NULL,
depth INT NOT NULL,
parent BIGINT NULL,
);
```
Comments store their content directly. Comments are not complex, and are
typically shorter than primary content. The practical limit for PostgreSQL is
1gb per row, but ShortForm imposes a configurable limit (10kb by default).
typically shorter than posts. The practical limit for PostgreSQL is 1gb per row,
but ShortForm imposes a configurable limit (10kb by default).
### Table: `assets`
@ -95,13 +97,13 @@ CREATE TABLE tags(
Tags are arbitrary labels that authors may assign to top level posts and assets.
### Table: `content_tags`
### Table: `post_tags`
```sql
CREATE TABLE content_tags(
content_id BIGINT NOT NULL,
CREATE TABLE post_tags(
post_id BIGINT NOT NULL,
tag_id BIGINT NOT NULL,
PRIMARY KEY(content_id, tag_id)
PRIMARY KEY(post_id, tag_id)
);
```

View file

@ -53,11 +53,11 @@ CREATE TYPE user_status AS ENUM ('active', 'locked', 'initializing');
```sql
CREATE TABLE users(
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
created_at TIMESTAMPTZ NOT NULL,
username TEXT NOT NULL UNIQUE,
password TEXT NOT NULL,
role user_role NOT NULL,
status user_status NOT NULL
status user_status NOT NULL,
created_at TIMESTAMPTZ NOT NULL
);
```
@ -66,9 +66,9 @@ CREATE TABLE users(
```sql
CREATE TABLE password_resets(
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
token TEXT NOT NULL UNIQUE,
created_at TIMESTAMPTZ NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
token TEXT NOT NULL,
used BOOLEAN NOT NULL
);
```

View file

@ -0,0 +1,61 @@
package gs.shortform.model
import gs.uuid.UUID
/**
* Represents a comment. All comments are associated to some [[Post]], but
* may also be replies to other comments.
*
* Comments cannot currently be edited.
*
* @param externalId Globally unique identifier for this comment.
* @param createdAt Instant this comment was created.
* @param createdBy User who created this comment.
* @param rendered The pre-rendered text contents of this comment.
* @param depth The depth of this comment.
* @param parent The parent comment of this comment.
*/
case class Comment(
externalId: UUID,
createdAt: CreatedAt,
createdBy: Username,
rendered: String,
depth: Comment.Depth,
parent: Option[UUID]
)
object Comment:
/**
* Represents comment depth. Used for rendition purposes. A depth of 0
* represents a top-level comment on some post.
*/
opaque type Depth = Int
object Depth:
/**
* Instantiate a new [[Depth]], forcing a minimum value of 0.
*
* @param value The value, rounded up to 0 if negative.
* @return The new [[Depth]].
*/
def apply(value: Int): Depth =
if value < 0 then 0 else value
given CanEqual[Depth, Depth] = CanEqual.derived
extension (depth: Depth)
/**
* @return The integer value of this depth.
*/
def toInt(): Int = depth
/**
* @return New [[Depth]] that is one level deeper.
*/
def increment(): Depth = toInt() + 1
end Depth
end Comment

View file

@ -1,25 +0,0 @@
package gs.shortform.model
import gs.uuid.UUID
import gs.shortform.crypto.Hash
/**
* Represents a top level piece of content - an article, essay, prompt, or some
* other written piece of work. All content must have a [[Title]] for display
* purposes.
*
* Content cannot currently be edited.
*
* @param externalId Globally unique identifier for this content.
* @param createdAt Instant this content was created.
* @param createdBy User who created this content.
* @param title Display title for this content.
* @param hash Hash of the primary rendered file for this content.
*/
case class Content(
externalId: UUID,
createdAt: CreatedAt,
createdBy: Username,
title: Title,
hash: Hash
)

View file

@ -0,0 +1,33 @@
package gs.shortform.model
import java.time.Instant
import java.time.OffsetDateTime
import java.time.ZoneOffset
/**
* Represents the instant some resource expires.
*/
opaque type ExpiresAt = Instant
object ExpiresAt:
def apply(value: Instant): ExpiresAt = value
def fromOffsetDateTime(value: OffsetDateTime): ExpiresAt = value.toInstant()
given CanEqual[ExpiresAt, ExpiresAt] = CanEqual.derived
extension (expiresAt: ExpiresAt)
/**
* Convert this value to an `Instant`.
*/
def toInstant(): Instant = expiresAt
/**
* Convert this value to an `OffsetDateTime` with the UTC offset.
*/
def toOffsetDateTime(): OffsetDateTime =
toInstant().atOffset(ZoneOffset.UTC)
end ExpiresAt

View file

@ -0,0 +1,17 @@
package gs.shortform.model
/**
* Represents a _password reset token_ that a user can use to set a new
* password.
*
* @param token The unique token.
* @param used Whether this token has been used.
* @param createdAt The instant this token was created.
* @param expiresAt The instant this token expires.
*/
case class PasswordReset(
token: String,
used: Boolean,
createdAt: CreatedAt,
expiresAt: ExpiresAt
)

View file

@ -0,0 +1,25 @@
package gs.shortform.model
import gs.uuid.UUID
import gs.shortform.crypto.Hash
/**
* Represents a top level piece of content - an article, essay, prompt, or some
* other written piece of work. All posts must have a [[Title]] for display
* purposes.
*
* Posts cannot currently be edited.
*
* @param externalId Globally unique identifier for this post.
* @param createdAt Instant this post was created.
* @param createdBy User who created this post.
* @param title Display title for this post.
* @param hash Hash of the primary rendered file for this post.
*/
case class Post(
externalId: UUID,
createdAt: CreatedAt,
createdBy: Username,
title: Title,
hash: Hash
)