Expanding the model and updating documentation.
This commit is contained in:
parent
e172d782b3
commit
ca2a85c81b
7 changed files with 152 additions and 39 deletions
|
@ -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)
|
||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
||||||
)
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
)
|
25
modules/model/src/main/scala/gs/shortform/model/Post.scala
Normal file
25
modules/model/src/main/scala/gs/shortform/model/Post.scala
Normal 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
|
||||||
|
)
|
Loading…
Add table
Reference in a new issue