Implement default SQLite connection

- Add logging to SQLite table creation
- Create common admin layout template
This commit is contained in:
Daniel J. Summers 2022-06-22 20:31:20 -04:00
parent cfc9472a2e
commit 409b1def32
8 changed files with 122 additions and 160 deletions

View File

@ -866,13 +866,13 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger<R
member _.startUp () = backgroundTask {
let! dbs = rethink<string list> { dbList; result; withRetryOnce conn }
if not (dbs |> List.contains config.Database) then
log.LogInformation($"Creating database {config.Database}...")
log.LogInformation $"Creating database {config.Database}..."
do! rethink { dbCreate config.Database; write; withRetryOnce; ignoreResult conn }
let! tables = rethink<string list> { tableList; result; withRetryOnce conn }
for tbl in Table.all do
if not (tables |> List.contains tbl) then
log.LogInformation($"Creating table {tbl}...")
log.LogInformation $"Creating table {tbl}..."
do! rethink { tableCreate tbl; write; withRetryOnce; ignoreResult conn }
do! ensureIndexes Table.Category [ "webLogId" ]

View File

@ -4,6 +4,7 @@ open System
open System.IO
open System.Threading.Tasks
open Microsoft.Data.Sqlite
open Microsoft.Extensions.Logging
open MyWebLog
open MyWebLog.ViewModels
@ -235,7 +236,7 @@ module private SqliteHelpers =
/// SQLite myWebLog data implementation
type SQLiteData (conn : SqliteConnection) =
type SQLiteData (conn : SqliteConnection, log : ILogger<SQLiteData>) =
/// Add parameters for category INSERT or UPDATE statements
let addCategoryParameters (cmd : SqliteCommand) (cat : Category) =
@ -1800,6 +1801,7 @@ type SQLiteData (conn : SqliteConnection) =
let! exists = tableExists "theme"
if not exists then
log.LogInformation "Creating theme tables..."
use cmd = conn.CreateCommand ()
cmd.CommandText <-
"""CREATE TABLE theme (
@ -1825,6 +1827,7 @@ type SQLiteData (conn : SqliteConnection) =
let! exists = tableExists "web_log"
if not exists then
log.LogInformation "Creating web log tables..."
use cmd = conn.CreateCommand ()
cmd.CommandText <-
"""CREATE TABLE web_log (
@ -1870,6 +1873,7 @@ type SQLiteData (conn : SqliteConnection) =
let! exists = tableExists "category"
if not exists then
log.LogInformation "Creating category table..."
use cmd = conn.CreateCommand ()
cmd.CommandText <-
"""CREATE TABLE category (
@ -1883,6 +1887,7 @@ type SQLiteData (conn : SqliteConnection) =
let! exists = tableExists "web_log_user"
if not exists then
log.LogInformation "Creating user table..."
use cmd = conn.CreateCommand ()
cmd.CommandText <-
"""CREATE TABLE web_log_user (
@ -1900,6 +1905,7 @@ type SQLiteData (conn : SqliteConnection) =
let! exists = tableExists "page"
if not exists then
log.LogInformation "Creating page tables..."
use cmd = conn.CreateCommand ()
cmd.CommandText <-
"""CREATE TABLE page (
@ -1937,6 +1943,7 @@ type SQLiteData (conn : SqliteConnection) =
let! exists = tableExists "post"
if not exists then
log.LogInformation "Creating post tables..."
use cmd = conn.CreateCommand ()
cmd.CommandText <-
"""CREATE TABLE post (
@ -1998,6 +2005,7 @@ type SQLiteData (conn : SqliteConnection) =
let! exists = tableExists "tag_map"
if not exists then
log.LogInformation "Creating tag map tables..."
use cmd = conn.CreateCommand ()
cmd.CommandText <-
"""CREATE TABLE tag_map (

View File

@ -38,19 +38,23 @@ module DataImplementation =
open RethinkDb.Driver.Net
/// Get the configured data implementation
let get (sp : IServiceProvider) : IData option =
let get (sp : IServiceProvider) : IData =
let config = sp.GetRequiredService<IConfiguration> ()
if (config.GetSection "RethinkDB").Exists () then
if (config.GetConnectionString >> isNull >> not) "SQLite" then
let conn = new SqliteConnection (config.GetConnectionString "SQLite")
SQLiteData.setUpConnection conn |> Async.AwaitTask |> Async.RunSynchronously
upcast SQLiteData (conn, sp.GetRequiredService<ILogger<SQLiteData>> ())
elif (config.GetSection "RethinkDB").Exists () then
Json.all () |> Seq.iter Converter.Serializer.Converters.Add
let rethinkCfg = DataConfig.FromConfiguration (config.GetSection "RethinkDB")
let conn = rethinkCfg.CreateConnectionAsync () |> Async.AwaitTask |> Async.RunSynchronously
Some (upcast RethinkDbData (conn, rethinkCfg, sp.GetRequiredService<ILogger<RethinkDbData>> ()))
elif (config.GetConnectionString >> isNull >> not) "SQLite" then
let conn = new SqliteConnection (config.GetConnectionString "SQLite")
SQLiteData.setUpConnection conn |> Async.AwaitTask |> Async.RunSynchronously
Some (upcast SQLiteData conn)
upcast RethinkDbData (conn, rethinkCfg, sp.GetRequiredService<ILogger<RethinkDbData>> ())
else
None
let log = sp.GetRequiredService<ILogger<SQLiteData>> ()
log.LogInformation "Using default SQLite database myweblog.db"
let conn = new SqliteConnection ("Data Source=./myweblog.db;Cache=Shared")
SQLiteData.setUpConnection conn |> Async.AwaitTask |> Async.RunSynchronously
upcast SQLiteData (conn, log)
open Giraffe
@ -78,39 +82,37 @@ let rec main args =
let _ = builder.Services.AddAuthorization ()
let _ = builder.Services.AddAntiforgery ()
let sp = builder.Services.BuildServiceProvider ()
match DataImplementation.get sp with
| Some data ->
task {
do! data.startUp ()
do! WebLogCache.fill data
do! ThemeAssetCache.fill data
} |> Async.AwaitTask |> Async.RunSynchronously
let sp = builder.Services.BuildServiceProvider ()
let data = DataImplementation.get sp
// Define distributed cache implementation based on data implementation
match data with
| :? RethinkDbData as rethink ->
// A RethinkDB connection is designed to work as a singleton
builder.Services.AddSingleton<IData> data |> ignore
builder.Services.AddDistributedRethinkDBCache (fun opts ->
opts.TableName <- "Session"
opts.Connection <- rethink.Conn)
|> ignore
| :? SQLiteData ->
// ADO.NET connections are designed to work as per-request instantiation
let cfg = sp.GetRequiredService<IConfiguration> ()
builder.Services.AddScoped<SqliteConnection> (fun sp ->
let conn = new SqliteConnection (cfg.GetConnectionString "SQLite")
SQLiteData.setUpConnection conn |> Async.AwaitTask |> Async.RunSynchronously
conn)
|> ignore
builder.Services.AddScoped<IData, SQLiteData> () |> ignore
// Use SQLite for caching as well
let cachePath = defaultArg (Option.ofObj (cfg.GetConnectionString "SQLiteCachePath")) "./session.db"
builder.Services.AddSqliteCache (fun o -> o.CachePath <- cachePath) |> ignore
| _ -> ()
| None ->
invalidOp "There is no data configuration present; please add a RethinkDB section or LiteDB connection string"
task {
do! data.startUp ()
do! WebLogCache.fill data
do! ThemeAssetCache.fill data
} |> Async.AwaitTask |> Async.RunSynchronously
// Define distributed cache implementation based on data implementation
match data with
| :? RethinkDbData as rethink ->
// A RethinkDB connection is designed to work as a singleton
builder.Services.AddSingleton<IData> data |> ignore
builder.Services.AddDistributedRethinkDBCache (fun opts ->
opts.TableName <- "Session"
opts.Connection <- rethink.Conn)
|> ignore
| :? SQLiteData as sql ->
// ADO.NET connections are designed to work as per-request instantiation
let cfg = sp.GetRequiredService<IConfiguration> ()
builder.Services.AddScoped<SqliteConnection> (fun sp ->
let conn = new SqliteConnection (sql.Conn.ConnectionString)
SQLiteData.setUpConnection conn |> Async.AwaitTask |> Async.RunSynchronously
conn)
|> ignore
builder.Services.AddScoped<IData, SQLiteData> () |> ignore
// Use SQLite for caching as well
let cachePath = Option.ofObj (cfg.GetConnectionString "SQLiteCachePath") |> Option.defaultValue "./session.db"
builder.Services.AddSqliteCache (fun o -> o.CachePath <- cachePath) |> ignore
| _ -> ()
let _ = builder.Services.AddSession(fun opts ->
opts.IdleTimeout <- TimeSpan.FromMinutes 60

View File

@ -0,0 +1,54 @@
<header>
<nav class="navbar navbar-dark bg-dark navbar-expand-md justify-content-start px-2 position-fixed top-0 w-100">
<div class="container-fluid">
<a class="navbar-brand" href="{{ "" | relative_link }}" hx-boost="false">{{ web_log.name }}</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarText"
aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarText">
{% if logged_on -%}
<ul class="navbar-nav">
{{ "admin/dashboard" | nav_link: "Dashboard" }}
{{ "admin/pages" | nav_link: "Pages" }}
{{ "admin/posts" | nav_link: "Posts" }}
{{ "admin/categories" | nav_link: "Categories" }}
{{ "admin/settings" | nav_link: "Settings" }}
</ul>
{%- endif %}
<ul class="navbar-nav flex-grow-1 justify-content-end">
{% if logged_on -%}
{{ "admin/user/edit" | nav_link: "Edit User" }}
{{ "user/log-off" | nav_link: "Log Off" }}
{%- else -%}
{{ "user/log-on" | nav_link: "Log On" }}
{%- endif %}
</ul>
</div>
</div>
</nav>
</header>
<main class="mx-3 mt-3">
<div class="messages mt-2" id="msgContainer">
{% for msg in messages %}
<div role="alert" class="alert alert-{{ msg.level }} alert-dismissible fade show">
{{ msg.message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
{% if msg.detail %}
<hr>
{{ msg.detail.value }}
{% endif %}
</div>
{% endfor %}
</div>
{{ content }}
</main>
<footer class="position-fixed bottom-0 w-100">
<div class="container-fluid">
<div class="row">
<div class="col-xs-12 text-end">
<img src="{{ "themes/admin/logo-light.png" | relative_link }}" alt="myWebLog" width="120" height="34">
</div>
</div>
</div>
</footer>

View File

@ -1,63 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>{{ page_title | escape }} &laquo; Admin &laquo; {{ web_log.name | escape }}</title>
<title>{{ page_title | strip_html }} &laquo; Admin &laquo; {{ web_log.name | strip_html }}</title>
</head>
<body>
<header>
<nav class="navbar navbar-dark bg-dark navbar-expand-md justify-content-start px-2 position-fixed top-0 w-100">
<div class="container-fluid">
<a class="navbar-brand" href="{{ "" | relative_link }}" hx-boost="false">{{ web_log.name }}</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarText"
aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarText">
{% if logged_on -%}
<ul class="navbar-nav">
{{ "admin/dashboard" | nav_link: "Dashboard" }}
{{ "admin/pages" | nav_link: "Pages" }}
{{ "admin/posts" | nav_link: "Posts" }}
{{ "admin/categories" | nav_link: "Categories" }}
{{ "admin/settings" | nav_link: "Settings" }}
</ul>
{%- endif %}
<ul class="navbar-nav flex-grow-1 justify-content-end">
{% if logged_on -%}
{{ "admin/user/edit" | nav_link: "Edit User" }}
{{ "user/log-off" | nav_link: "Log Off" }}
{%- else -%}
{{ "user/log-on" | nav_link: "Log On" }}
{%- endif %}
</ul>
</div>
</div>
</nav>
</header>
<main class="mx-3 mt-3">
<div class="messages mt-2" id="msgContainer">
{% for msg in messages %}
<div role="alert" class="alert alert-{{ msg.level }} alert-dismissible fade show">
{{ msg.message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
{% if msg.detail %}
<hr>
{{ msg.detail.value }}
{% endif %}
</div>
{% endfor %}
</div>
{{ content }}
</main>
<footer class="position-fixed bottom-0 w-100">
<div class="container-fluid">
<div class="row">
<div class="col-xs-12 text-end">
<img src="{{ "themes/admin/logo-light.png" | relative_link }}" alt="myWebLog" width="120" height="34">
</div>
</div>
</div>
</footer>
{% include_template "_layout" %}
<script>Admin.dismissSuccesses()</script>
</body>
</html>

View File

@ -3,66 +3,13 @@
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="generator" content="{{ generator }}">
<title>{{ page_title | escape }} &laquo; Admin &laquo; {{ web_log.name | escape }}</title>
<title>{{ page_title | strip_html }} &laquo; Admin &laquo; {{ web_log.name | strip_html }}</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css"
integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
<link rel="stylesheet" href="{{ "themes/admin/admin.css" | relative_link }}">
</head>
<body hx-boost="true">
<header>
<nav class="navbar navbar-dark bg-dark navbar-expand-md justify-content-start px-2 position-fixed top-0 w-100">
<div class="container-fluid">
<a class="navbar-brand" href="{{ "" | relative_link }}" hx-boost="false">{{ web_log.name }}</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarText"
aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarText">
{% if logged_on -%}
<ul class="navbar-nav">
{{ "admin/dashboard" | nav_link: "Dashboard" }}
{{ "admin/pages" | nav_link: "Pages" }}
{{ "admin/posts" | nav_link: "Posts" }}
{{ "admin/categories" | nav_link: "Categories" }}
{{ "admin/settings" | nav_link: "Settings" }}
</ul>
{%- endif %}
<ul class="navbar-nav flex-grow-1 justify-content-end">
{% if logged_on -%}
{{ "admin/user/edit" | nav_link: "Edit User" }}
{{ "user/log-off" | nav_link: "Log Off" }}
{%- else -%}
{{ "user/log-on" | nav_link: "Log On" }}
{%- endif %}
</ul>
</div>
</div>
</nav>
</header>
<main class="mx-3 mt-3">
<div class="messages mt-2" id="msgContainer">
{% for msg in messages %}
<div role="alert" class="alert alert-{{ msg.level }} alert-dismissible fade show">
{{ msg.message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
{% if msg.detail %}
<hr>
{{ msg.detail.value }}
{% endif %}
</div>
{% endfor %}
</div>
{{ content }}
</main>
<footer class="position-fixed bottom-0 w-100">
<div class="container-fluid">
<div class="row">
<div class="col-xs-12 text-end">
<img src="{{ "themes/admin/logo-light.png" | relative_link }}" alt="myWebLog" width="120" height="34">
</div>
</div>
</div>
</footer>
{% include_template "_layout" %}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js"
integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM"
crossorigin="anonymous"></script>

View File

@ -1,7 +1,11 @@
{% if model.subtitle %}
<h2>{{ model.subtitle.value }}</h2>
{% endif %}
<section class="container" aria-label="The posts for the page">
{%- if is_category or is_tag %}
<h1 class="index-title">{{ page_title }}</h1>
{%- if is_category %}
{%- assign cat = categories | where: "slug", slug | first -%}
{%- if cat.description %}<h4 class="text-muted">{{ cat.description.value }}</h4>{% endif -%}
{%- endif %}
{%- endif %}
<section class="container mt-3" aria-label="The posts for the page">
{% for post in model.posts %}
<article>
<h1>

View File

@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css"
integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
<title>{{ page_title | escape }}{% if page_title %} &laquo; {% endif %}{{ web_log.name | escape }}</title>
<title>{{ page_title | strip_html }}{% if page_title %} &laquo; {% endif %}{{ web_log.name | strip_html }}</title>
{% page_head -%}
</head>
<body>
@ -19,7 +19,7 @@
</button>
<div class="collapse navbar-collapse" id="navbarText">
{% if web_log.subtitle -%}
<span class="navbar-text">{{ web_log.subtitle.value | escape }}</span>
<span class="navbar-text">{{ web_log.subtitle.value }}</span>
{%- endif %}
<ul class="navbar-nav">
{% unless web_log.default_page == "posts" %}{{ "page/1" | nav_link: "Posts" }}{% endunless %}