GithubHelp home page GithubHelp logo

zaid-ajaj / tabula-rasa Goto Github PK

View Code? Open in Web Editor NEW
199.0 18.0 16.0 12.99 MB

Minimalistic real-worldish blogging platform, written entirely in F#, made as a learning reference for building large Elmish apps

HTML 0.52% CSS 1.36% F# 95.30% JavaScript 1.51% Shell 0.32% Batchfile 0.09% Sass 0.89%
elmish fsharp safe-stack full-stack remoting litedb

tabula-rasa's Introduction

Tabula Rasa Build Status

A minimalistic real-worldish blog engine written entirely in F#. Specifically made as a learning resource when building apps with the SAFE stack. This application features many concerns of large apps such as:

  • Using third-party react libraries via interop
  • Deep nested views
  • Deep nested routing
  • Message interception as means for component communication
  • Logging
  • Database access
  • User security: authentication and authorization
  • Type-safe RPC communication
  • Realtime type-safe messaging via web sockets

Screen recordings

first.gif

second.gif

bridge.gif

The server uses the following tech

The client uses the following tech

Communication Protocol

To understand how the application works and what it does, you simply take a look the protocol between the client and server:

type IBlogApi = {  
    getBlogInfo : unit -> Async<Result<BlogInfo, string>>
    login : LoginInfo -> Async<LoginResult>
    getPosts : unit -> Async<list<BlogPostItem>>
    getPostBySlug : string -> Async<Option<BlogPostItem>>
    getDrafts : AuthToken -> SecureResponse<list<BlogPostItem>>
    publishNewPost : SecureRequest<NewBlogPostReq> -> SecureResponse<AddPostResult> 
    savePostAsDraft : SecureRequest<NewBlogPostReq> -> SecureResponse<AddPostResult>
    deleteDraftById : SecureRequest<int> -> SecureResponse<DeleteDraftResult>
    publishDraft : SecureRequest<int> -> SecureResponse<PublishDraftResult>
    deletePublishedArticleById : SecureRequest<int> -> SecureResponse<DeletePostResult>
    turnArticleToDraft: SecureRequest<int> -> SecureResponse<MakeDraftResult>
    getPostById : SecureRequest<int> -> SecureResponse<Option<BlogPostItem>>
    savePostChanges : SecureRequest<BlogPostItem> -> SecureResponse<Result<bool, string>>
    updateBlogInfo : SecureRequest<BlogInfo> -> SecureResponse<Result<SuccessMsg, ErrorMsg>>
    togglePostFeatured : SecureRequest<int> -> SecureResponse<Result<string, string>>
    updatePassword : SecureRequest<UpdatePasswordInfo> -> SecureResponse<Result<string, string>> 
}

Thanks to Fable.Remoting, this application does not need to handle data serialization/deserialization and routing between client and server, it is all done for us which means that the code is 99% domain models and domain logic.

You will often see calls made to server from the client like these:

| ToggleFeatured postId ->
    let nextState = { state with IsTogglingFeatured = Some postId }
    let request = { Token = authToken; Body = postId }
    let toggleFeatureCmd = 
        Cmd.fromAsync {
            Value = Server.api.togglePostFeatured request
            Error = fun ex -> ToggleFeaturedFinished (Error "Network error while toggling post featured")
            Success = function 
                | Error authError -> ToggleFeaturedFinished (Error "User was unauthorized")
                | Ok toggleResult -> ToggleFeaturedFinished toggleResult
        } 
    nextState, toggleFeatureCmd

Client Application Layout

The client application layout is how the components are structured in the project. The components are written in a consistent pattern that is reflected by the file system as follows:

ParentComponent 
   | 
   | - Types.fs
   | - State.fs
   | - View.fs
   | - ChildComponent
        | 
        | - Types.fs
        | - State.fs
        | - View.fs

Where the client is a tree of UI components:

App 
 |
 | - About
 | - Posts 
      | 
      | - SinglePost
      | - AllPosts 
 |
 | - Admin
      | 
      | - Login
      | - Backoffice
           | 
           | - PublishedArticles
           | - Drafts 
           | - Settings 
           | - NewArticle
           | - EditArticle 

Component Types

Every component comes with a Types.fs file that contains mostly three things

  • State data model that the component keeps track of
  • Msg type that represents the events that can occur
  • Pages represents the current page and sub pages that a component can have

The State keeps track of the CurrentPage but it will never update it by hand: the CurrentPage is only updated in response to url changes and these changes will dispatch a message to change the value of the CurrentPage along with dispatching other messages related to loading the data for the component

Important Concepts: Data Locality and Message Interception

Following these principles to help us write components in isolation:

  • Child components don't know anything about their parents
  • Child components don't know anything about their siblings
  • Parent components manage child state and communication between children

The best example of these concepts is the interaction between the following components:

        Admin
          |
   ---------------
   |             |
Backoffice     Login     

Message Interception by example

Definition: Message interception is having control over how messages flow in your application, allowing for communication between components that don't know each other even exist.

Login doesn't know anything going on in the application as a whole, it just has a form for the user to input his credentials and try to login to the server to obtain an authorization token. When the token is obtained, a LoginSuccess token message is dispatched. However, this very message is intercepted by Admin (the parent of Login), updating the state of Admin:

// Admin/State.fs

let update msg (state: State) =
    match msg with
    | LoginMsg loginMsg ->
        match loginMsg with 
        // intercept the LoginSuccess message dispatched by the child component
        | Login.Types.Msg.LoginSuccess token ->
            let nextState = 
                { state with Login = state.Login
                             SecurityToken = Some token }
            nextState, Urls.navigate [ Urls.admin ] 
        // propagate other messages to child component
        | _ -> 
            let nextLoginState, nextLoginCmd = Admin.Login.State.update loginMsg state.Login
            let nextAdminState = { state with Login = nextLoginState }
            nextAdminState, Cmd.map LoginMsg nextLoginCmd

After updating the state of Admin to include the security token obtained from Login, the application navigates to the admin pages using Urls.navigate [ Urls.admin ]. Now the navigation will succeed, because navigating to the admin is allowed only if the admin has a security token defined:

// App/State.fs -> inside handleUpdatedUrl

| Admin.Types.Page.Backoffice backofficePage ->
    match state.Admin.SecurityToken with
    | None -> 
        // navigating to one of the admins backoffice pages 
        // without a security token? then you need to login first
        Cmd.batch [ Urls.navigate [ Urls.login ]
                    showInfo "You must be logged in first" ] 
    | Some userSecurityToken ->
        // then user is already logged in 
        // for each specific page, dispatch the appropriate message 
        // for initial loading of that data of that page
        match backofficePage with 
        | Admin.Backoffice.Types.Page.Drafts -> 
            Admin.Backoffice.Drafts.Types.LoadDrafts
            |> Admin.Backoffice.Types.Msg.DraftsMsg
            |> Admin.Types.Msg.BackofficeMsg 
            |> AdminMsg 
            |> Cmd.ofMsg
        
        | Admin.Backoffice.Types.Page.PublishedPosts -> 
            Admin.Backoffice.PublishedPosts.Types.LoadPublishedPosts
            |> Admin.Backoffice.Types.Msg.PublishedPostsMsg
            |> Admin.Types.Msg.BackofficeMsg
            |> AdminMsg
            |> Cmd.ofMsg 

        | Admin.Backoffice.Types.Page.Settings ->
            Admin.Backoffice.Settings.Types.Msg.LoadBlogInfo
            |> Admin.Backoffice.Types.Msg.SettingsMsg
            |> Admin.Types.Msg.BackofficeMsg
            |> AdminMsg
            |> Cmd.ofMsg 
         
        | Admin.Backoffice.Types.Page.EditArticle postId ->
            Admin.Backoffice.EditArticle.Types.Msg.LoadArticleToEdit postId 
            |> Admin.Backoffice.Types.Msg.EditArticleMsg 
            |> Admin.Types.Msg.BackofficeMsg
            |> AdminMsg 
            |> Cmd.ofMsg 
        
        | otherPage -> 
            Cmd.none

Another concrete example in this application: when you update the settings, the root component intercepts the "Changed settings" message and reloads it's blog information with the new settings accordingly

Data Locality by example

Definition: Data Locality is having control over the data that is available to certain components, without access to global state.

Fact: Components of Backoffice need to make secure requests, hence they need a security token available whenever a request is to be made.

Requirement: Once the user is inside a component of Backoffice, there will always be a SecurityToken available to that component. This is because I don't want to check whether there is a security token or not everytime I want to make a web request, because if there isn't one, there is an internal inconsistency: the user shouldn't have been able to reach the Backoffice component in the first place.

Problem: The security token is only acquired after the user logs in from Login, but before that there isn't a security token, hence the type of the token will be SecurityToken: string option but we don't want an optional token, we want an actual token once we are logged in.

Solution: Login and components of Backoffice cannot be siblings, Login is happy with the security token being optional, while Backoffice insists on having a token at any given time. So we introduce a parent: Admin that handles the optionalness of the security token! The Admin will disallow the user from reaching Backoffice if there isn't a security token, and if there is one, it will be propagated to the backoffice:

// Admin/State.fs -> update
| BackofficeMsg msg ->
    match msg with 
    | Backoffice.Types.Msg.Logout -> 
        // intercept logout message of the backoffice child
        let nextState, _ = init()
        nextState, Urls.navigate [ Urls.posts ]
    | _ -> 
        match state.SecurityToken with 
        | Some token -> 
            let prevBackofficeState = state.Backoffice
            let nextBackofficeState, nextBackofficeCmd = 
                // pass security token down to backoffice
                Backoffice.State.update token msg prevBackofficeState
            let nextAdminState = { state with Backoffice = nextBackofficeState }
            nextAdminState, Cmd.map BackofficeMsg nextBackofficeCmd
        | None ->
            state, Cmd.none

Unit-testable at the composition root level:

The composition root is where the application functionality gets all the dependencies it needs to run to application like the database and a logger. In this application, the composition root is where we construct an implementation for the IBlogApi protocol:

let liftAsync x = async { return x }

/// Composition root of the application
let createBlogApi (logger: ILogger) (database: LiteDatabase) : IBlogApi = 
     // create initial admin guest admin if one does not exists
    Admin.writeAdminIfDoesNotExists database Admin.guestAdmin 
    let getBlogInfo() = async { return Admin.blogInfo database }
    let getPosts() = async { return BlogPosts.getPublishedArticles database } 
    let blogApi : IBlogApi = {   
        getBlogInfo = getBlogInfo
        getPosts = getPosts 
        login = Admin.login logger database >> liftAsync
        publishNewPost = BlogPosts.publishNewPost logger database
        getPostBySlug =  BlogPosts.getPostBySlug database >> liftAsync
        savePostAsDraft = BlogPosts.saveAsDraft logger database 
        getDrafts = BlogPosts.getAllDrafts database
        deleteDraftById = BlogPosts.deleteDraft logger database 
        publishDraft = BlogPosts.publishDraft database
        deletePublishedArticleById = BlogPosts.deletePublishedArticle database 
        turnArticleToDraft = BlogPosts.turnArticleToDraft database
        getPostById = BlogPosts.getPostById database 
        savePostChanges = BlogPosts.savePostChanges database
        updateBlogInfo = Admin.updateBlogInfo database
        togglePostFeatured = BlogPosts.togglePostFeatured database 
        updatePassword = Admin.updatePassword logger database
    }

    blogApi

Because LiteDB already includes an in-memory database and Serilog provides a simple no-op logger, you can write unit tests right off the bat at the application level:

// creates a disposable in memory database
let useDatabase (f: LiteDatabase -> unit) = 
    let mapper = FSharpBsonMapper()
    use memoryStream = new MemoryStream()
    use db = new LiteDatabase(memoryStream, mapper)
    f db

testCase "Login with default credentials works" <| fun _ -> 
    useDatabase <| fun db -> 
        let logger = Serilog.Log.Logger 
        let testBlogApi = WebApp.createBlogApi logger db 
        let loginInfo = { Username = "guest"; Password = "guest" }
        let result = Async.RunSynchronously (testBlogApi.login loginInfo)
        match result with 
        | LoginResult.Success token -> pass() 
        | _ -> fail()

Of course you can also test the individual functions seperately because every function is also unit testable as long as you provide a database instance and a logger.

Responsive using different UI's

As opposed to using CSS to show or hide elements based on screen size, I used react-responsive to make a completely different app for small-sized screens, implemented as

let app blogInfo state dispatch =
  div 
   [ ]
   [ mediaQuery 
      [ MinWidth 601 ]
      [ desktopApp blogInfo state dispatch ]
     mediaQuery 
      [ MaxWidth 600 ] 
      [ mobileApp blogInfo state dispatch ] ]

Security with JWT

User authentication and authorization happen though secure requests, these requests include the JSON web token to authorize the user. The user acquires these JWT's when logging in and everything is stateless. An example of a secure request with it's handler on the server:

// Client

| ToggleFeatured postId ->
    let nextState = { state with IsTogglingFeatured = Some postId }
    let request = { Token = authToken; Body = postId }
    let toggleFeatureCmd = 
        Cmd.fromAsync {
            Value = Server.api.togglePostFeatured request
            Error = fun ex -> ToggleFeaturedFinished (Error "Network error while toggling post featured")
            Success = function 
                | Error authError -> ToggleFeaturedFinished (Error "User was unauthorized")
                | Ok toggleResult -> ToggleFeaturedFinished toggleResult
        } 
    nextState, toggleFeatureCmd

And it is handled like this on the server:

// Server

let togglePostFeatured (db: LiteDatabase) = 
    Security.authorizeAdmin <| fun postId admin -> 
        let posts = db.GetCollection<BlogPost> "posts"
        match posts.tryFindOne <@ fun post -> post.Id = postId @> with 
        | None -> Error "Blog post could not be found"
        | Some post -> 
            let modifiedPost = { post with IsFeatured = not post.IsFeatured }
            if posts.Update modifiedPost 
            then Ok "Post was successfully updated" 
            else Error "Error occured while updating the blog post"

See Modeling Authentication and Authorization in Fable.Remoting to learn more

Try out on your machine

Requirements:

Start watch build on windows:

git clone https://github.com/Zaid-Ajaj/tabula-rasa.git 
cd tabula-rasa
build.cmd Watch 

On linux/mac you can use bash

git clone https://github.com/Zaid-Ajaj/tabula-rasa.git 
cd tabula-rasa
./build.sh Watch

This will start the build and create the LiteDb (single file) database for the first time if it does not already exist. The database will be in the application data directory of your OS under the tabula-rasa directory with name TabulaRasa.db along with the newly generated secret key used for generating secure Json web tokens. The "application data directory" on most linux systems will be ~/.config/, resulting in a directory ~/.config/tabula-rasa/.

When the build finishes, you can navigate to http://localhost:8090 to start using the application. Once you make changes to either server or client, it will automatically re-compile the app.

Once the application starts, the home page will tell you "There aren't any stories published yet" because the database is still empty. You can then navigate to http://localhost:8090/#login to login in as an admin who can write stories. The default credentials are Username = guest and Password = guest.

More

There is a lot to talk about with this application, but the best way to learn from it is by actually trying it out and going through the code yourself. If you need clarification or explanation on why a code snippet is written the way it is, just open an issue with your question :)

tabula-rasa's People

Contributors

akoslukacs avatar dependabot[bot] avatar draptik avatar fergusmeiklejohn avatar isaacabraham avatar mattherman avatar thedevknight avatar zaid-ajaj avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

tabula-rasa's Issues

Fix build

Looks like travis ci is not happy anymore, this must due to the fact that fable-loader wasn't entirely locked and now requires fable-compiler as a peer dependency.

Error creating a database file on macOS (simple fix included)

When the server tries to initialize the database by running "new LiteDatabase(dbFile, mapper)", it throws the error:

System.InvalidOperationException: Your platform does not support FileStream.Lock. Please set mode=Exclusive in your connnection string to avoid this error.

Fixed by simply replacing the following: (Server>Storage.fs)

new LiteDatabase(dbFile, mapper)

With:

let cnn = ConnectionString(sprintf "filename=%s; mode=Exclusive" dbFile)
new LiteDatabase(cnn, mapper)

Running build.cmd Watch leads to errors

Installed these packages:
dotnet-sdk-6.0.402-win-x64.exe
node-v18.12.0-x64.msi
npm install -g yarn
1.22.19

Running build.cmd Watch leads to these error messages:

Paket version 7.2.0+bb14ab674b2748070a624f394cd6796e11aaffa8
The last full restore is still up to date. Nothing left to do.
Total time taken: 0 milliseconds
Building project with version: LocalBuild
Shortened DependencyGraph for Target Watch:
<== Watch
<== DotnetRestore
<== Clean
<== NpmInstall

The running order is:

  • Clean
  • NpmInstall
  • DotnetRestore
  • Watch
    Running build with 1 worker
    Starting Target: Clean
    Deleting contents of Server\bin
    Deleting contents of Server\obj
    Deleting contents of Server.Tests\bin
    Deleting contents of Server.Tests\obj
    Deleting contents of Client\src\bin
    Deleting contents of Client\src\obj
    Finished Target: Clean
    Starting Target: NpmInstall (==> Clean, Clean, Clean)
    CWD: Client
    cmd /C yarn install
    yarn install v1.22.19
    [1/4] Resolving packages...
    [2/4] Fetching packages...
    [3/4] Linking dependencies...
    warning " > [email protected]" has incorrect peer dependency "react@^16.0.0".
    [4/4] Building fresh packages...
    [-/2] โข€ waiting...
    error e:\SAFE Stack\tabula-rasa\Client\node_modules\node-sass: Command failed.
    Exit code: 1
    Command: node scripts/build.js
    Arguments:
    Directory: e:\SAFE Stack\tabula-rasa\Client\node_modules\node-sass
    Output:
    Building: C:\Program Files\nodejs\node.exe e:\SAFE Stack\tabula-rasa\Client\node_modules\node-gyp\bin\node-gyp.js rebuild --verbose --libsass_ext= --libsass_cflags= --libsass_ldflags= --libsass_library=
    gyp info it worked if it ends with ok
    gyp verb cli [
    gyp verb cli 'C:\Program Files\nodejs\node.exe',
    gyp verb cli 'e:\SAFE Stack\tabula-rasa\Client\node_modules\node-gyp\bin\node-gyp.js',
    gyp verb cli 'rebuild',
    gyp verb cli '--verbose',
    gyp verb cli '--libsass_ext=',
    gyp verb cli '--libsass_cflags=',
    gyp verb cli '--libsass_ldflags=',
    gyp verb cli '--libsass_library='
    gyp verb cli ]
    gyp info using [email protected]
    gyp info using [email protected] | win32 | x64
    gyp verb command rebuild []
    gyp verb command clean []
    gyp verb clean removing "build" directory
    gyp verb command configure []
    gyp verb check python checking for Python executable "python2" in the PATH
    gyp verb which failed Error: not found: python2
    gyp verb which failed at getNotFoundError (e:\SAFE Stack\tabula-rasa\Client\node_modules\which\which.js:13:12)
    gyp verb which failed at F (e:\SAFE Stack\tabula-rasa\Client\node_modules\which\which.js:68:19)
    gyp verb which failed at E (e:\SAFE Stack\tabula-rasa\Client\node_modules\which\which.js:80:29)
    gyp verb which failed at e:\SAFE Stack\tabula-rasa\Client\node_modules\which\which.js:89:16
    gyp verb which failed at e:\SAFE Stack\tabula-rasa\Client\node_modules\isexe\index.js:42:5
    gyp verb which failed at e:\SAFE Stack\tabula-rasa\Client\node_modules\isexe\windows.js:36:5
    gyp verb which failed at FSReqCallback.oncomplete (node:fs:207:21)
    gyp verb which failed python2 Error: not found: python2
    gyp verb which failed at getNotFoundError (e:\SAFE Stack\tabula-rasa\Client\node_modules\which\which.js:13:12)
    gyp verb which failed at F (e:\SAFE Stack\tabula-rasa\Client\node_modules\which\which.js:68:19)
    gyp verb which failed at E (e:\SAFE Stack\tabula-rasa\Client\node_modules\which\which.js:80:29)
    gyp verb which failed at e:\SAFE Stack\tabula-rasa\Client\node_modules\which\which.js:89:16
    gyp verb which failed at e:\SAFE Stack\tabula-rasa\Client\node_modules\isexe\index.js:42:5
    gyp verb which failed at e:\SAFE Stack\tabula-rasa\Client\node_modules\isexe\windows.js:36:5
    gyp verb which failed at FSReqCallback.oncomplete (node:fs:207:21) {
    gyp verb which failed code: 'ENOENT'
    gyp verb which failed }
    gyp verb check python checking for Python executable "python" in the PATH
    gyp verb which succeeded python C:\Users\User\AppData\Local\Programs\Python\Python311\python.EXE
    gyp ERR! configure error
    gyp ERR! stack Error: Command failed: C:\Users\User\AppData\Local\Programs\Python\Python311\python.EXE -c import sys; print "%s.%s.%s" % sys.version_info[:3];
    gyp ERR! stack File "", line 1
    gyp ERR! stack import sys; print "%s.%s.%s" % sys.version_info[:3];
    gyp ERR! stack ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    gyp ERR! stack SyntaxError: Missing parentheses in call to 'print'. Did you mean print(...)?
    gyp ERR! stack
    gyp ERR! stack at ChildProcess.exithandler (node:child_process:412:12)
    gyp ERR! stack at ChildProcess.emit (node:events:513:28)
    gyp ERR! stack at maybeClose (node:internal/child_process:1091:16)
    gyp ERR! stack at Socket. (node:internal/child_process:449:11)
    gyp ERR! stack at Socket.emit (node:events:513:28)
    gyp ERR! stack at Pipe. (node:net:313:12)
    gyp ERR! System Windows_NT 10.0.19044
    gyp ERR! command "C:\Program Files\nodejs\node.exe" "e:\SAFE Stack\tabula-rasa\Client\node_modules\node-gyp\bin\node-gyp.js" "rebuild" "--verbose" "--libsass_ext=" "--libsass_cflags=" "--libsass_ldflags=" "--libsass_library="
    gyp ERR! cwd e:\SAFE Stack\tabula-rasa\Client\node_modules\node-sass
    gyp ERR! node -v v18.12.0
    gyp ERR! node-gyp -v v3.8.0
    gyp ERR! not ok
    Build failed with error code: 1

Running build failed.
Error:
System.Exception: 'Client> cmd /C yarn install' task failed
at FSI_0005.Build.run(String fileName, String args, String workingDir)
at [email protected](Unit _arg4)
at Fake.TargetHelper.runSingleTarget(TargetTemplate`1 target) in D:\code\fake\src\app\FakeLib\TargetHelper.fs:line 626


Build Time Report

Target Duration


Clean 00:00:00.0073197
NpmInstall Failure
Total: 00:00:05.4454510

Status: Failure


  1. System.Exception: 'Client> cmd /C yarn install' task failed
    at FSI_0005.Build.run(String fileName, String args, String workingDir)
    at [email protected](Unit _arg4)
    at Fake.TargetHelper.runSingleTarget(TargetTemplate`1 target) in D:\code\fake\src\app\FakeLib\TargetHelper.fs:line 626

e:\SAFE Stack\tabula-rasa>

.build.sh file does not work

I'm trying to run this example in my computer, but after run .build.sh i get the following error:

./build.sh Watch                                               
dirname: missing operand
Try 'dirname --help' for more information.
./build.sh: line 12: mono: command not found

I'm using ubuntu 20.04 LTS and My dotnet version is 3.1.405

How to login?

I tried to login as "admin", but it returns UsernameDoesNotExist.

How do I know what credentials to use?

Where is the database? I searched for *.db, but found nothing.

I assume once I can login as an admin, then it will be obvious how to add other logins.

Error while running build.cmd Watch

Just cloned the repo and ran 'build.cmd Watch'. It keeps producing error as:
C:\Program Files\dotnet\sdk\3.0.100\NuGet.targets(123,5): error : End of Central Directory record could not be found. [D:\Work\iprnd\fs\ZaidAjaj\tabula-rasa\Client\src\TabulaRasa.Client.fsproj]
Running build failed.

Any suggestions?

Here is complete console output:
Paket version 5.241.6
Extracted Paket.Restore.targets to: D:\Work\iprnd\fs\ZaidAjaj\tabula-rasa.paket\Paket.Restore.targets (Can be disabled with PAKET_SKIP_RESTORE_TARGETS=true)
Starting full restore process.
Performance:

  • Disk IO: 771 milliseconds
  • Runtime: 2 seconds
    Building project with version: LocalBuild
    Shortened DependencyGraph for Target Watch:
    <== Watch
    <== DotnetRestore
    <== Clean
    <== NpmInstall

The running order is:

  • Clean
  • NpmInstall
  • DotnetRestore
  • Watch
    Running build with 1 worker
    Starting Target: Clean
    Creating D:\Work\iprnd\fs\ZaidAjaj\tabula-rasa\Server\bin
    Deleting contents of Server\obj
    Creating D:\Work\iprnd\fs\ZaidAjaj\tabula-rasa\Server.Tests\bin
    Deleting contents of Server.Tests\obj
    Creating D:\Work\iprnd\fs\ZaidAjaj\tabula-rasa\Client\src\bin
    Deleting contents of Client\src\obj
    Finished Target: Clean
    Starting Target: NpmInstall (==> Clean, Clean, Clean)
    CWD: Client
    cmd /C yarn install
    yarn install v1.12.3
    [1/4] Resolving packages...
    [2/4] Fetching packages...
    info [email protected]: The platform "win32" is incompatible with this module.
    info "[email protected]" is an optional dependency and failed compatibility check. Excluding it from installation.
    [3/4] Linking dependencies...
    warning " > [email protected]" has incorrect peer dependency "react@^16.0.0".
    [4/4] Building fresh packages...
    Done in 99.29s.
    Finished Target: NpmInstall
    Starting Target: DotnetRestore (==> Clean, NpmInstall, NpmInstall, NpmInstall)
    CWD: Server
    cmd /C dotnet restore --no-cache
    Paket version 5.241.6
    The last restore is still up to date. Nothing left to do.
    Performance:
  • Runtime: 170 milliseconds
    Paket version 5.241.6
    Starting restore process.
    Performance:
  • Runtime: 1 second
    Restore completed in 1.88 sec for D:\Work\iprnd\fs\ZaidAjaj\tabula-rasa\Server\TabulaRasa.Server.fsproj.
    CWD: Server.Tests
    cmd /C dotnet restore --no-cache
    Paket version 5.241.6
    The last restore is still up to date. Nothing left to do.
    Performance:
  • Runtime: 398 milliseconds
    Paket version 5.241.6
    Starting restore process.
    Performance:
  • Runtime: 2 seconds
    Restore completed in 370.55 ms for D:\Work\iprnd\fs\ZaidAjaj\tabula-rasa\Server\TabulaRasa.Server.fsproj.
    Restore completed in 505.75 ms for D:\Work\iprnd\fs\ZaidAjaj\tabula-rasa\Server.Tests\Server.Tests.fsproj.
    CWD: Client\src
    cmd /C dotnet restore --no-cache
    Paket version 5.241.6
    The last restore is still up to date. Nothing left to do.
    Performance:
  • Runtime: 305 milliseconds
    Paket version 5.241.6
    Starting restore process.
    Performance:
  • Runtime: 1 second
    Restore completed in 768.18 ms for D:\Work\iprnd\fs\ZaidAjaj\tabula-rasa\Client\src\TabulaRasa.Client.fsproj.
    C:\Program Files\dotnet\sdk\3.0.100\NuGet.targets(123,5): error : End of Central Directory record could not be found. [D:\Work\iprnd\fs\ZaidAjaj\tabula-rasa\Client\src\TabulaRasa.Client.fsproj]
    Running build failed.
    Error:
    System.Exception: 'Client\src> cmd /C dotnet restore --no-cache' task failed
    at FSI_0005.Build.run(String fileName, String args, String workingDir) in D:\Work\iprnd\fs\ZaidAjaj\tabula-rasa\build.fsx:line 16
    at [email protected](String workingDir) in D:\Work\iprnd\fs\ZaidAjaj\tabula-rasa\build.fsx:line 29
    at Microsoft.FSharp.Primitives.Basics.List.iter[T](FSharpFunc2 f, FSharpList1 x)
    at [email protected](Unit _arg2) in D:\Work\iprnd\fs\ZaidAjaj\tabula-rasa\build.fsx:line 29
    at Fake.TargetHelper.runSingleTarget(TargetTemplate`1 target) in D:\code\fake\src\app\FakeLib\TargetHelper.fs:line 626

Build Time Report

Target Duration


Clean 00:00:00.0104677
NpmInstall 00:01:39.9851236
DotnetRestore Failure
Total: 00:01:56.4905687

Status: Failure


  1. System.Exception: 'Client\src> cmd /C dotnet restore --no-cache' task failed
    at FSI_0005.Build.run(String fileName, String args, String workingDir) in D:\Work\iprnd\fs\ZaidAjaj\tabula-rasa\build.fsx:line 16
    at [email protected](String workingDir) in D:\Work\iprnd\fs\ZaidAjaj\tabula-rasa\build.fsx:line 29
    at Microsoft.FSharp.Primitives.Basics.List.iter[T](FSharpFunc2 f, FSharpList1 x)
    at [email protected](Unit _arg2) in D:\Work\iprnd\fs\ZaidAjaj\tabula-rasa\build.fsx:line 29
    at Fake.TargetHelper.runSingleTarget(TargetTemplate`1 target) in D:\code\fake\src\app\FakeLib\TargetHelper.fs:line 626

Websockets don't work with Firefox?

On a clean build from the latest version of the repository, whilst I can get "live changes" to occur when publishing posts in Chrome, it doesn't work in Firefox. Also - is it supposed to work for other actions than publishing e.g. update, delete etc.?

How to get started on Windows

What is the best way to start running this application on Windows?

There is no build.cmd file. Can I run the script using build.sh on Windows?

Thanks

Port to Giraffe?

This is an amazing project, thank you.

I found it while trying to find a safe-stack starter that used LiteDB.FSharp.

Unfortunately Saturn (safe-stack, netcoreapp3.x) precludes the use of LiteDB (at least, I got framework incompatibility errors that I don't yet know how to resolve). So I searched again and ended up here, linked from the safe site.

So this project looks to be safe-friendly, maybe apart from the use of Suave (they seem to imply you'd want to move away from Suave). So, I'd like to ask:

  • What's your opinion on Suave vs Giraffe?
  • Is this project portable to Giraffe?
  • Is it worth doing?

Thanks again for this project and LiteDB.FSharp.

Rewrite with Feliz

  • Rewrite in Feliz
  • Update Fable and dependencies
  • Use Discriminated Union composition to implement child pages (fixes #2)

Store only active page state in the model

With the current AppState, you are storing a lot of information.

In general, when you are on the admin section , you don't need the Posts section state. For this reason, instead of creating one property in a record per children I prefer to use a DU.

Another reason in favor of this, is it's make the update function quicker. Because you can reduce the number of nested record and also need to copy less data.

// Proposition

type ActivePage =
    | Admin of Admin.Types.State
    | Posts of Posts.Types.State

type AppState = {
    // App's own state
    BlogInfo: Remote<BlogInfo>
    ActivePage: ActivePage
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.