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