Skip to content

Commit

Permalink
feat: save and visualize posts if they're published
Browse files Browse the repository at this point in the history
  • Loading branch information
AngelMunoz committed Oct 27, 2024
1 parent ebaf721 commit fcc11f7
Show file tree
Hide file tree
Showing 6 changed files with 195 additions and 56 deletions.
31 changes: 30 additions & 1 deletion Controllers/Public.fs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,41 @@ module Controllers.Public
open Oxpecker

open Views
open Models
open Services

let index: EndpointHandler =
fun ctx ->
let env = ctx.GetLayoutEnv()
let posts = ctx.GetService<PostsService>()
let getSummaries = fun () -> posts.findPostSummaries()

sendHox (Public.index(env)) ctx
sendHox (Public.index(getSummaries, env)) ctx

let postDetail(path: string) : EndpointHandler =
fun ctx -> task {
let env = ctx.GetLayoutEnv()
let posts = ctx.GetService<PostsService>()

let idx = path.LastIndexOf("_")

let title = path.Substring(0, idx) |> System.Web.HttpUtility.UrlDecode
let slug = path.Substring(idx + 1)

let! post = posts.findPostByTitleAndSlug(title, slug)

return!
match post with
| Some(post) ->
jsonChunked
{|
post with
status = post.status.AsString
|}
ctx
| None -> (setStatusCode 404 >=> text "Not Found") ctx

}

let about: EndpointHandler =
fun ctx ->
Expand Down
9 changes: 9 additions & 0 deletions Models.fs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ type Post = {
publishedAt: DateTime option
}

type PostSummary = {
id: Guid
title: string
summary: string
authorName: string
permanentPath: string
publishedAt: DateTime
}

type NewPostPayload = {
title: string
content: string
Expand Down
1 change: 1 addition & 0 deletions Program.fs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ app
route "/" Controllers.Public.index
route "/about" Controllers.Public.about
route "/posts" Controllers.Posts.newPost
routef "/posts/%s" Controllers.Public.postDetail
]
CsrfPost [ route "/posts" Controllers.Posts.savePost ]
]
Expand Down
131 changes: 96 additions & 35 deletions Services/Database.fs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ open Donald

open Models
open NpgsqlTypes
open System.Web

type ConnectionFactory =
abstract member CreateConnectionAsync: unit -> Task<IDbConnection>
Expand All @@ -34,6 +35,8 @@ type PostsService =
abstract savePost: NewPostPayload -> Task<unit>
abstract findPostById: Guid -> Task<Post option>
abstract updatePost: Post -> Task<unit>
abstract findPostSummaries: unit -> Task<PostSummary list>
abstract findPostByTitleAndSlug: string * string -> Task<Post option>

type AuthorsService =
abstract findAuthors: unit -> Task<Author list>
Expand All @@ -56,11 +59,6 @@ module Mappings =
socialNetworks = Map.empty
}
| Some id ->

let name = reader.ReadString("author_name")
let email = reader.ReadString("author_email")
let bio = reader.ReadString("author_bio")

let socialNetworks =
reader.ReadStringOption("author_social_networks")
|> Option.map(
Expand All @@ -70,40 +68,38 @@ module Mappings =

{
id = id
name = name
email = email
bio = bio
name = reader.ReadString("author_name")
email = reader.ReadString("author_email")
bio = reader.ReadString("author_bio")
socialNetworks = socialNetworks
}

let postMapper authorMapper (reader: IDataReader) =
let id = reader.ReadGuid("id")
let title = reader.ReadString("title")
let content = reader.ReadString("content")

let status =
reader.ReadString("status")
let postMapper authorMapper (reader: IDataReader) = {
id = reader.ReadGuid("id")
title = reader.ReadString("title")
content = reader.ReadString("content")
status =
reader.ReadString "status"
|> function
| "draft" -> Draft
| "published" -> Published
| _ -> failwith "Invalid Post Status"

let createdAt = reader.ReadDateTime("created_at")
let updatedAt = reader.ReadDateTime("updated_at")
let publishedAt = reader.ReadDateTimeOption("published_at")
let slug = reader.ReadStringOption("slug")

{
id = id
title = title
content = content
status = status
slug = slug
author = authorMapper reader
createdAt = createdAt
updatedAt = updatedAt
publishedAt = publishedAt
}
slug = reader.ReadStringOption("slug")
createdAt = reader.ReadDateTime("created_at")
updatedAt = reader.ReadDateTime("updated_at")
publishedAt = reader.ReadDateTimeOption("published_at")
author = authorMapper reader
}

let summaryLikeMapper(reader: IDataReader) = {|
id = reader.ReadGuid("id")
title = reader.ReadString("title")
content = reader.ReadString("content")
publishedAt = reader.ReadDateTime("published_at")
slug = reader.ReadString("slug")
authorName =
reader.ReadStringOption("author_name") |> Option.defaultValue "Unknown"
|}

module Queries =
[<Literal>]
Expand All @@ -119,6 +115,33 @@ module Queries =
LEFT JOIN
authors a ON p.author = a.id"""

[<Literal>]
let selectPostSummary =
"""
SELECT
p.id as id, p.title as title, p.content as content,
p.published_at as published_at, p.slug as slug, a.name as author_name
FROM
posts p
LEFT JOIN
authors a ON p.author_id = a.id
WHERE p.status = 'published'
"""

[<Literal>]
let postByTitleAndSlug =
"""
SELECT
p.id as id, p.title as title, p.content as content, p.status as status, p.slug as slug,
p.created_at as created_at , p.updated_at as updated_at, p.published_at as published_at,
a.id as author_id, a.name as author_name, a.email as author_email, a.bio as author_bio,
a.social_networks as author_social_networks
FROM
posts p
LEFT JOIN
authors a ON p.author_id = a.id
WHERE p.title = @title AND p.slug = @slug"""

[<Literal>]
let insertPost =
"""
Expand Down Expand Up @@ -298,14 +321,52 @@ module Posts =
return if result = 0 then failwith "Failed to save post" else ()
}

member _.findPostSummaries() = task {
use! connection = connectionFactory.CreateConnectionAsync()

let! summaryLike =
connection
|> Db.newCommand Queries.selectPostSummary
|> Db.Async.query(Mappings.summaryLikeMapper)

let summaries =
summaryLike
|> List.map(fun sLike -> {
id = sLike.id
title = sLike.title
summary = sLike.content[0..100]
authorName = sLike.authorName
permanentPath =
$"/posts/{HttpUtility.HtmlEncode(sLike.title)}_{sLike.slug}"
publishedAt = sLike.publishedAt
})

return summaries
}

member _.findPostByTitleAndSlug(title, slug) = task {
use! connection = connectionFactory.CreateConnectionAsync()

let! postWithAuthor =
connection
|> Db.newCommand Queries.postByTitleAndSlug
|> Db.setParams [
"title", SqlType.String title
"slug", SqlType.String slug
]
|> Db.Async.querySingle(Mappings.postMapper Mappings.authorMapper)

return postWithAuthor
}

}


module Authors =

let getAuthorService(connectionFactory: ConnectionFactory) =
{ new AuthorsService with
member this.findAuthors() = task {
member _.findAuthors() = task {
use! connection = connectionFactory.CreateConnectionAsync()

let! authors =
Expand All @@ -316,7 +377,7 @@ module Authors =
return authors
}

member this.saveAuthor(author) = task {
member _.saveAuthor(author) = task {
let! connection = connectionFactory.CreateConnectionAsync()

use cmd =
Expand Down Expand Up @@ -345,7 +406,7 @@ module Authors =
return if result = 0 then failwith "Failed to save author" else ()
}

member this.updateAuthor(author) = task {
member _.updateAuthor(author) = task {

let! connection = connectionFactory.CreateConnectionAsync()

Expand Down
32 changes: 18 additions & 14 deletions Views/Posts.fs
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,30 @@ open Oxpecker
open Hox
open Layout

// if the file gets "too big" we can split it into multiple files and just add
// more extension members to tis type
type Posts = class end


module Components =
module Posts =
module Components =

let getAuthorOptions(getAuthors: unit -> Task<Author list>) =
fragment(
task {
let! authors = getAuthors()
let getAuthorOptions(getAuthors: unit -> Task<Author list>) =
fragment(
task {
let! authors = getAuthors()

return seq {
h("option[value=null]", text "Select an author")
return seq {
h("option[value=null]", text "Select an author")

for author in authors do
h($"option[value={author.id}", $"{author.name} - {author.email}")
for author in authors do
h($"option[value={author.id}", $"{author.name} - {author.email}")
}
}
}
)
)

open Posts

// if the file gets "too big" we can split it into multiple files and just add
// more extension members to tis type
type Posts = class end

type Posts with

Expand Down
47 changes: 41 additions & 6 deletions Views/Public.fs
Original file line number Diff line number Diff line change
@@ -1,26 +1,61 @@
namespace Views

open System.Threading.Tasks

open Hox
open Layout
open Models

module Public =

module Components =

let postSummaryItems getPosts =
fragment(
task {

let! posts = getPosts()

return seq {
for post in posts do
h(
"li",
h(
"header",
h($"a[href={post.permanentPath}]", h("h3", post.title))
),
h(
"section",
h(
"div.uk-flex",
h("div", post.authorName),
h("div", post.publishedAt.ToShortDateString())
),
h("p", post.summary)
)
)
}
}
)

open Public

// if the file gets "too big" we can split it into multiple files and just add
// more extension members to this type
type Public = class end


type Public with

static member index(?env: LayoutEnv) =
static member index(postListSummary, ?env: LayoutEnv) =
Page(
h(
"article.uk-padding",
h(
"header",
h("h1", "Hola! Bienvenido a la pagina del Ompo"),
h("h1", "Somewhat of a blog here's a few things"),
h(
"p.uk-text-large",
text "Tambien conocido como Victor. Tiene un hermano llamado: ",
h("a[href=/about].uk-link-text", "Pompo")
"ul.uk-list.uk-list-large.uk-list-striped",
Components.postSummaryItems postListSummary
)
)
),
Expand Down

0 comments on commit fcc11f7

Please sign in to comment.