Index tweaks

also:
- created modules for converters and indexes
- ensure we use our JSON converters for both RavenDB and Giraffe
- first cut at a composable function pipeline instead of method chaining for IWebHostBuilder
This commit is contained in:
Daniel J. Summers 2019-07-28 22:21:05 -05:00
parent a2ef84bc1e
commit 87ce966ca1
2 changed files with 175 additions and 137 deletions

View File

@ -9,65 +9,79 @@ open Raven.Client.Documents.Linq
open System
open System.Collections.Generic
/// JSON converter for request IDs
type RequestIdJsonConverter () =
inherit JsonConverter<RequestId> ()
override __.WriteJson(writer : JsonWriter, value : RequestId, _ : JsonSerializer) =
(RequestId.toString >> writer.WriteValue) value
override __.ReadJson(reader: JsonReader, _ : Type, _ : RequestId, _ : bool, _ : JsonSerializer) =
(string >> RequestId.fromIdString) reader.Value
/// JSON converters for various DUs
module Converters =
/// JSON converter for request IDs
type RequestIdJsonConverter () =
inherit JsonConverter<RequestId> ()
override __.WriteJson(writer : JsonWriter, value : RequestId, _ : JsonSerializer) =
(RequestId.toString >> writer.WriteValue) value
override __.ReadJson(reader: JsonReader, _ : Type, _ : RequestId, _ : bool, _ : JsonSerializer) =
(string >> RequestId.fromIdString) reader.Value
/// JSON converter for user IDs
type UserIdJsonConverter () =
inherit JsonConverter<UserId> ()
override __.WriteJson(writer : JsonWriter, value : UserId, _ : JsonSerializer) =
(UserId.toString >> writer.WriteValue) value
override __.ReadJson(reader: JsonReader, _ : Type, _ : UserId, _ : bool, _ : JsonSerializer) =
(string >> UserId) reader.Value
/// JSON converter for user IDs
type UserIdJsonConverter () =
inherit JsonConverter<UserId> ()
override __.WriteJson(writer : JsonWriter, value : UserId, _ : JsonSerializer) =
(UserId.toString >> writer.WriteValue) value
override __.ReadJson(reader: JsonReader, _ : Type, _ : UserId, _ : bool, _ : JsonSerializer) =
(string >> UserId) reader.Value
/// JSON converter for Ticks
type TicksJsonConverter () =
inherit JsonConverter<Ticks> ()
override __.WriteJson(writer : JsonWriter, value : Ticks, _ : JsonSerializer) =
(Ticks.toLong >> writer.WriteValue) value
override __.ReadJson(reader: JsonReader, _ : Type, _ : Ticks, _ : bool, _ : JsonSerializer) =
(string >> int64 >> Ticks) reader.Value
/// A sequence of all custom converters for myPrayerJournal
let all : JsonConverter seq =
seq {
yield RequestIdJsonConverter ()
yield UserIdJsonConverter ()
yield TicksJsonConverter ()
}
/// JSON converter for Ticks
type TicksJsonConverter () =
inherit JsonConverter<Ticks> ()
override __.WriteJson(writer : JsonWriter, value : Ticks, _ : JsonSerializer) =
(Ticks.toLong >> writer.WriteValue) value
override __.ReadJson(reader: JsonReader, _ : Type, _ : Ticks, _ : bool, _ : JsonSerializer) =
(string >> int64 >> Ticks) reader.Value
/// RavenDB index declarations
module Indexes =
/// Index requests by user ID
type Requests_ByUserId () as this =
inherit AbstractJavaScriptIndexCreationTask ()
do
this.Maps <- HashSet<string> [ "docs.Requests.Select(req => new { userId = req.userId })" ]
/// Index requests by user ID
type Requests_ByUserId () as this =
inherit AbstractJavaScriptIndexCreationTask ()
do
this.Maps <- HashSet<string> [ "map('Requests', function (req) { return { userId : req.userId } })" ]
/// Index requests for a journal view
type Requests_AsJournal () as this =
inherit AbstractJavaScriptIndexCreationTask ()
do
this.Maps <- HashSet<string> [
"docs.Requests.Select(req => new {
requestId = req.Id,
userId = req.userId,
text = req.history.Where(hist => hist.text != null).OrderByDescending(hist => hist.asOf).First().text,
asOf = req.history.OrderByDescending(hist => hist.asOf).First().asOf,
snoozedUntil = req.snoozedUntil,
showAfter = req.showAfter,
recurType = req.recurType,
recurCount = req.recurCount
})"
]
this.Fields <-
[ "text", IndexFieldOptions (Storage = Nullable FieldStorage.Yes)
"asOf", IndexFieldOptions (Storage = Nullable FieldStorage.Yes)
]
|> dict
|> Dictionary<string, IndexFieldOptions>
/// Index requests for a journal view
type Requests_AsJournal () as this =
inherit AbstractJavaScriptIndexCreationTask ()
do
this.Maps <- HashSet<string> [
"map('Requests', function (req) {
var hist = req.history
.filter(function (hist) { return hist.text !== null })
.sort(function (a, b) { return b - a })
return {
requestId : req.Id,
userId : req.userId,
text : hist[0].text,
asOf : req.history[req.history.length - 1].asOf,
snoozedUntil : req.snoozedUntil,
showAfter : req.showAfter,
recurType : req.recurType,
recurCount : req.recurCount
}
})"
]
/// Extensions on the IAsyncDocumentSession interface to support our data manipulation needs
[<AutoOpen>]
module Extensions =
open Indexes
open Raven.Client.Documents.Commands.Batches
open Raven.Client.Documents.Operations
open Raven.Client.Documents.Session

View File

@ -2,81 +2,110 @@ namespace MyPrayerJournal.Api
open Microsoft.AspNetCore.Builder
open Microsoft.AspNetCore.Hosting
open System.IO
/// Configuration functions for the application
module Configure =
open Microsoft.Extensions.Configuration
open Newtonsoft.Json
/// Configure the content root
let contentRoot root (bldr : IWebHostBuilder) =
bldr.UseContentRoot root
/// Set up the configuration for the app
let configuration (ctx : WebHostBuilderContext) (cfg : IConfigurationBuilder) =
cfg.SetBasePath(ctx.HostingEnvironment.ContentRootPath)
.AddJsonFile("appsettings.json", optional = true, reloadOnChange = true)
.AddJsonFile(sprintf "appsettings.%s.json" ctx.HostingEnvironment.EnvironmentName)
.AddEnvironmentVariables ()
|> ignore
open Microsoft.Extensions.Configuration
/// Configure the application configuration
let appConfiguration (bldr : IWebHostBuilder) =
let configuration (ctx : WebHostBuilderContext) (cfg : IConfigurationBuilder) =
cfg.SetBasePath(ctx.HostingEnvironment.ContentRootPath)
.AddJsonFile("appsettings.json", optional = true, reloadOnChange = true)
.AddJsonFile(sprintf "appsettings.%s.json" ctx.HostingEnvironment.EnvironmentName)
.AddEnvironmentVariables ()
|> ignore
bldr.ConfigureAppConfiguration configuration
open Microsoft.AspNetCore.Server.Kestrel.Core
/// Configure Kestrel from appsettings.json
let kestrel (ctx : WebHostBuilderContext) (opts : KestrelServerOptions) =
(ctx.Configuration.GetSection >> opts.Configure >> ignore) "Kestrel"
let kestrel (bldr : IWebHostBuilder) =
let kestrel (ctx : WebHostBuilderContext) (opts : KestrelServerOptions) =
(ctx.Configuration.GetSection >> opts.Configure >> ignore) "Kestrel"
bldr.ConfigureKestrel kestrel
open Giraffe.Serialization
open Microsoft.FSharpLu.Json
/// Custom settings for the JSON serializer (uses compact representation for options and DUs)
let jsonSettings =
let x = NewtonsoftJsonSerializer.DefaultSettings
x.Converters.Add (CompactUnionJsonConverter (true))
x.NullValueHandling <- NullValueHandling.Ignore
x.MissingMemberHandling <- MissingMemberHandling.Error
x.Formatting <- Formatting.Indented
x
/// Configure the web root directory
let webRoot pathSegments (bldr : IWebHostBuilder) =
(Path.Combine >> bldr.UseWebRoot) pathSegments
open Giraffe
open Giraffe.Serialization
open Giraffe.TokenRouter
open Microsoft.AspNetCore.Authentication.JwtBearer
open Microsoft.Extensions.DependencyInjection
open Microsoft.FSharpLu.Json
open MyPrayerJournal
open MyPrayerJournal.Indexes
open Newtonsoft.Json
open Raven.Client.Documents
open Raven.Client.Documents.Indexes
open System.Security.Cryptography.X509Certificates
/// Configure dependency injection
let services (sc : IServiceCollection) =
use sp = sc.BuildServiceProvider ()
let cfg = sp.GetRequiredService<IConfiguration> ()
sc.AddGiraffe()
.AddAuthentication(
/// Use HTTP "Bearer" authentication with JWTs
fun opts ->
opts.DefaultAuthenticateScheme <- JwtBearerDefaults.AuthenticationScheme
opts.DefaultChallengeScheme <- JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(
/// Configure JWT options with Auth0 options from configuration
fun opts ->
let jwtCfg = cfg.GetSection "Auth0"
opts.Authority <- sprintf "https://%s/" jwtCfg.["Domain"]
opts.Audience <- jwtCfg.["Id"])
|> ignore
sc.AddSingleton<IJsonSerializer> (NewtonsoftJsonSerializer jsonSettings)
|> ignore
let config = sc.BuildServiceProvider().GetRequiredService<IConfiguration>().GetSection "RavenDB"
let store = new DocumentStore ()
store.Urls <- [| config.["URLs"] |]
store.Database <- config.["Database"]
store.Certificate <- new X509Certificate2 (config.["Certificate"], config.["Password"])
store.Conventions.CustomizeJsonSerializer <- (fun x ->
x.Converters.Add (RequestIdJsonConverter ())
x.Converters.Add (TicksJsonConverter ())
x.Converters.Add (UserIdJsonConverter ())
x.Converters.Add (CompactUnionJsonConverter true))
store.Initialize () |> sc.AddSingleton |> ignore
IndexCreation.CreateIndexes (typeof<Requests_ByUserId>.Assembly, store)
let services (bldr : IWebHostBuilder) =
let svcs (sc : IServiceCollection) =
/// A set of JSON converters used for both Giraffe's request serialization and RavenDB's storage
let jsonConverters : JsonConverter seq =
seq {
yield! Converters.all
yield CompactUnionJsonConverter true
}
/// Custom settings for the JSON serializer (uses compact representation for options and DUs)
let jsonSettings =
let x = NewtonsoftJsonSerializer.DefaultSettings
jsonConverters |> List.ofSeq |> List.iter x.Converters.Add
x.NullValueHandling <- NullValueHandling.Ignore
x.MissingMemberHandling <- MissingMemberHandling.Error
x.Formatting <- Formatting.Indented
x
use sp = sc.BuildServiceProvider ()
let cfg = sp.GetRequiredService<IConfiguration> ()
sc.AddGiraffe()
.AddAuthentication(
/// Use HTTP "Bearer" authentication with JWTs
fun opts ->
opts.DefaultAuthenticateScheme <- JwtBearerDefaults.AuthenticationScheme
opts.DefaultChallengeScheme <- JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(
/// Configure JWT options with Auth0 options from configuration
fun opts ->
let jwtCfg = cfg.GetSection "Auth0"
opts.Authority <- sprintf "https://%s/" jwtCfg.["Domain"]
opts.Audience <- jwtCfg.["Id"])
|> ignore
sc.AddSingleton<IJsonSerializer> (NewtonsoftJsonSerializer jsonSettings)
|> ignore
let config = sc.BuildServiceProvider().GetRequiredService<IConfiguration>().GetSection "RavenDB"
let store = new DocumentStore ()
store.Urls <- [| config.["URL"] |]
store.Database <- config.["Database"]
// store.Certificate <- new X509Certificate2 (config.["Certificate"], config.["Password"])
store.Conventions.CustomizeJsonSerializer <- fun x -> jsonConverters |> List.ofSeq |> List.iter x.Converters.Add
store.Initialize () |> (sc.AddSingleton >> ignore)
IndexCreation.CreateIndexes (typeof<Requests_ByUserId>.Assembly, store)
bldr.ConfigureServices svcs
open Microsoft.Extensions.Logging
/// Configure logging
let logging (bldr : IWebHostBuilder) =
let logz (log : ILoggingBuilder) =
let env = log.Services.BuildServiceProvider().GetService<IHostingEnvironment> ()
match env.IsDevelopment () with
| true -> log
| false -> log.AddFilter(fun l -> l > LogLevel.Information)
|> function l -> l.AddConsole().AddDebug()
|> ignore
bldr.ConfigureLogging logz
/// Routes for the available URLs within myPrayerJournal
let webApp =
router Handlers.Error.notFound [
@ -108,50 +137,45 @@ module Configure =
]
]
open System
/// Configure the web application
let application (app : IApplicationBuilder) =
let env = app.ApplicationServices.GetService<IHostingEnvironment> ()
match env.IsDevelopment () with
| true -> app.UseDeveloperExceptionPage ()
| false -> app.UseGiraffeErrorHandler Handlers.Error.error
|> function
| a ->
a.UseAuthentication()
.UseStaticFiles()
.UseGiraffe webApp
|> ignore
let application (bldr : IWebHostBuilder) =
let appConfig =
Action<IApplicationBuilder> (
fun (app : IApplicationBuilder) ->
let env = app.ApplicationServices.GetService<IHostingEnvironment> ()
match env.IsDevelopment () with
| true -> app.UseDeveloperExceptionPage ()
| false -> app.UseGiraffeErrorHandler Handlers.Error.error
|> function
| a ->
a.UseAuthentication()
.UseStaticFiles()
.UseGiraffe webApp
|> ignore)
bldr.Configure appConfig
open Microsoft.Extensions.Logging
/// Configure logging
let logging (log : ILoggingBuilder) =
let env = log.Services.BuildServiceProvider().GetService<IHostingEnvironment> ()
match env.IsDevelopment () with
| true -> log
| false -> log.AddFilter(fun l -> l > LogLevel.Information)
|> function l -> l.AddConsole().AddDebug()
|> ignore
/// Compose all the configurations into one
let webHost appRoot pathSegments =
contentRoot appRoot
>> appConfiguration
>> kestrel
>> webRoot (Array.concat [ [| appRoot |]; pathSegments ])
>> services
>> logging
>> application
/// Build the web host from the given configuration
let buildHost (bldr : IWebHostBuilder) = bldr.Build ()
module Program =
open System
open System.IO
let exitCode = 0
let CreateWebHostBuilder _ =
let contentRoot = Directory.GetCurrentDirectory ()
WebHostBuilder()
.UseContentRoot(contentRoot)
.ConfigureAppConfiguration(Configure.configuration)
.UseKestrel(Configure.kestrel)
.UseWebRoot(Path.Combine (contentRoot, "wwwroot"))
.ConfigureServices(Configure.services)
.ConfigureLogging(Configure.logging)
.Configure(Action<IApplicationBuilder> Configure.application)
[<EntryPoint>]
let main args =
CreateWebHostBuilder(args).Build().Run()
let main _ =
let appRoot = Directory.GetCurrentDirectory ()
use host = WebHostBuilder () |> (Configure.webHost appRoot [| "wwwroot" |] >> Configure.buildHost)
host.Run ()
exitCode