Subscribe now

Server-Side Validations (Josh Adams and Luke Westby pairing) [09.09.2016]

In the last episode we made elm-simple-form and elm-mdl play nicely together for my increasingly-capable and open-source elm-mdl Single Page Application with a Phoenix API, time-tracker. We now get live client-side validations of our New User form, which is nice.

Of course all validations can't actually live on the client. A good example of this would be a unique email address requirement. Consequently, let's introduce server-side validations and make them play nicely with elm-simple-form.

NOTE: This is a pairing session with Josh Adams and Luke Westby. The text below explains both interesting timestamps in the video and attempts to succinctly describe the changes made in the session and their reasoning. I'm not yet sure if this sort of thing occasionally as an episode is awesome or jarring (because it's super long at 1 hour and 40 minutes), so hit the comments section and let me know!

Project

You can find the single commit representing all of our changes from this episode here. It's not overwhelmingly complicated in its final form, but if you want to watch the design evolve the video's pretty great I think. My audio on this video is much worse than it should be and I'm very sorry about that.

Interesting Timestamps

In general, for people that have watched the last few episodes, it gets to actual new stuff at 0:25:16 for the first time.

Here are some interesting timestamps from the video:

  • 00:00:00 - The intro, where I just begin familiarizing Luke with the project in general.
  • 00:00:42 - Quick(-ish) intro to elm-simple-form for people that haven't watched the last two episodes.
  • 00:04:12 - Beginning to walk through the issues we had to deal with to make elm-simple-form play nicely with elm-mdl.
  • 00:13:02 - The bit where I get distracted.
  • 00:19:48 - Getting an API error response's body (necessary to show our API-driven validation errors).
  • 00:25:16 - The Motivation.
  • 00:27:56 - A love-letter to Elm Json.Decode.Decoders.
  • 00:32:45 - Figuring out that elm-simple-form's Field Type is tree-like.
  • 00:37:15 - Designing our API Field Errors type.
  • 00:38:38 - Josh's relatively-newfound philosophy on over-engineering.
  • 00:41:08 - Back to API Field Errors.
  • 00:49:35 - Defining the decoder for our API Field Errors.
  • 00:53:48 - Adding a Validation to our Phoenix API (so we can get some server-side validations that aren't reproducible on the client side).
  • 01:02:26 - Trying out our Decoder.
  • 01:05:21 - Merging our Server-Side Errors with elm-simple-form.
  • 01:09:37 - I figured out how I wanted the form and its corresponding API errors to work in the Model.
  • 01:12:41 - Luke's Thought-Leadership on elm-package.
  • 01:26:14 - UX Cleanup - it mostly works well at this point but we have a few UX quirks we want to squash.

The commit, annotated.

Here's our committed code, once again. I'm going to paste in the diff itself and just annotate the changes we made and why they were made, as an attempt to make it easier for people that don't want to watch a super-long video:

Introduce a Decoder for our API's errors JSON

We need to turn the API response body, in the event of a failure, into our own custom type (which we show further down). Here's the Decoder we added.

diff --git a/elm/src/Decoders.elm b/elm/src/Decoders.elm
index df33364..f5eabd8 100644
--- a/elm/src/Decoders.elm
+++ b/elm/src/Decoders.elm
@@ -6,10 +6,12 @@ module Decoders
         , projectDecoder
         , organizationsDecoder
         , organizationDecoder
+        , apiFieldErrorsDecoder
         )

 import Json.Decode as JD exposing ((:=))
-import Types exposing (User, Project, Organization)
+import Types exposing (User, Project, Organization, APIFieldErrors)
+import Dict


 usersDecoder : JD.Decoder (List User)
@@ -46,3 +48,8 @@ organizationDecoder =
     JD.object2 Organization
         (JD.maybe ("id" := JD.int))
         ("name" := JD.string)
+
+
+apiFieldErrorsDecoder : JD.Decoder APIFieldErrors
+apiFieldErrorsDecoder =
+    "errors" := (JD.dict <| JD.list JD.string)

Combine our form and its (maybe) errors

We need to combine our form and its server-side errors in order to handle things reasonably in the view, so we just put them in a tuple. Also, we introduce the Validation for the form.

diff --git a/elm/src/Model.elm b/elm/src/Model.elm
index 49ff6b4..cb09fd5 100644
--- a/elm/src/Model.elm
+++ b/elm/src/Model.elm
@@ -4,7 +4,7 @@ import Msg exposing (Msg)
 import Material
 import Material.Snackbar as Snackbar
 import Route
-import Types exposing (User, Sorted, UserSortableField, Project, ProjectSortableField, Organization, OrganizationSortableField)
+import Types exposing (User, Sorted, UserSortableField, Project, ProjectSortableField, Organization, OrganizationSortableField, APIFieldErrors)
 import Form exposing (Form)
 import Form.Validate exposing (Validation, form1, get, string)

@@ -15,7 +15,7 @@ type alias Model =
     , baseUrl : String
     , route : Route.Model
     , users : List User
-    , newUserForm : Form () User
+    , newUserForm : ( Form String User, Maybe APIFieldErrors )
     , shownUser : Maybe User
     , usersSort : Maybe ( Sorted, UserSortableField )
     , projects : List Project
@@ -36,7 +36,7 @@ initialModel location =
     , baseUrl = "http://localhost:4000"
     , route = Route.init location
     , users = []
-    , newUserForm = Form.initial [] validateNewUser
+    , newUserForm = ( Form.initial [] validateNewUser, Nothing )
     , shownUser = Nothing
     , usersSort = Nothing
     , projects = []
@@ -50,7 +50,7 @@ initialModel location =
     }


-validateNewUser : Validation () User
+validateNewUser : Validation String User
 validateNewUser =
     form1 (User Nothing)
         (get "name" string)

Inroduce our APIFieldErrors type

diff --git a/elm/src/Msg.elm b/elm/src/Msg.elm
index e9fd104..e03a139 100644
--- a/elm/src/Msg.elm
+++ b/elm/src/Msg.elm
@@ -22,8 +22,6 @@ type Msg
 type UserMsg
     = GotUser User
     | GotUsers (List User)
-      -- | SetNewUserName String
-      -- | CreateUser
     | CreateUserSucceeded User
     | CreateUserFailed OurHttp.Error
     | DeleteUser User
diff --git a/elm/src/Types.elm b/elm/src/Types.elm
index 133b368..7c51752 100644
--- a/elm/src/Types.elm
+++ b/elm/src/Types.elm
@@ -7,8 +7,11 @@ module Types
         , ProjectSortableField(..)
         , Organization
         , OrganizationSortableField(..)
+        , APIFieldErrors
         )

+import Dict exposing (Dict)
+

 type alias User =
     { id : Maybe Int
@@ -43,3 +46,7 @@ type OrganizationSortableField
 type Sorted
     = Ascending
     | Descending
+
+
+type alias APIFieldErrors =
+    Dict String (List String)

Handle the update changes

We need to update our form-handling Update bits.

diff --git a/elm/src/Update.elm b/elm/src/Update.elm
index 33e9341..fabbd3e 100644
--- a/elm/src/Update.elm
+++ b/elm/src/Update.elm
@@ -2,7 +2,7 @@ module Update exposing (update)

 import Model exposing (Model)
 import Msg exposing (Msg(..), UserMsg(..), ProjectMsg(..), OrganizationMsg(..))
-import Types exposing (User, UserSortableField(..), Sorted(..), Project, ProjectSortableField(..), Organization, OrganizationSortableField(..))
+import Types exposing (User, UserSortableField(..), Sorted(..), Project, ProjectSortableField(..), Organization, OrganizationSortableField(..), APIFieldErrors)
 import Material
 import Material.Snackbar as Snackbar
 import Navigation
@@ -12,6 +12,8 @@ import OurHttp exposing (Error(..))
 import Http exposing (Value(..))
 import API
 import Form
+import Dict
+import Decoders


 update : Msg -> Model -> ( Model, Cmd Msg )
@@ -133,45 +135,46 @@ updateUserMsg msg model =
         GotUsers users ->
             { model | users = users } ! []

-        -- SetNewUserName name ->
-        --     let
-        --         oldNewUser =
-        --             model.newUser
-        --     in
-        --         { model | newUser = { oldNewUser | name = name } } ! []
-        --
-        -- CreateUser ->
-        --     model ! [ API.createUser model model.newUser (UserMsg' << CreateUserFailed) (UserMsg' << CreateUserSucceeded) ]
         CreateUserSucceeded _ ->
             { model
-              -- | newUser = initialModel.newUser
                 | newUserForm = initialModel.newUserForm
             }
                 ! [ Navigation.newUrl (Route.urlFor Users) ]

         CreateUserFailed error ->
             let
-                decodeError : OurHttp.Error -> String
+                decodeError : OurHttp.Error -> APIFieldErrors
                 decodeError error =
                     case error of
                         BadResponse code text value ->
-                            "error! - "
-                                ++ case value of
-                                    Text responseBody ->
-                                        case JD.decodeString JD.value responseBody of
-                                            Ok val ->
-                                                toString val
-
-                                            Err str ->
-                                                str
-
-                                    e ->
-                                        toString e
+                            case value of
+                                Text responseBody ->
+                                    JD.decodeString Decoders.apiFieldErrorsDecoder (Debug.log "r" responseBody)
+                                        |> Result.withDefault
+                                            ((Debug.log <|
+                                                "derp didn't get an api field errors decodable response back, instead got "
+                                                    ++ responseBody
+                                             )
+                                                Dict.empty
+                                            )
+
+                                e ->
+                                    Dict.empty
+                                        |> (Debug.log <|
+                                                "this is a blob how did that hapen? "
+                                                    ++ (toString error)
+                                           )

                         e ->
-                            toString e
+                            Dict.empty
+                                |> (Debug.log <|
+                                        "this is a blob how did that hapen? "
+                                            ++ (toString e)
+                                   )
             in
-                model ! [] |> andLog "Create User failed" (decodeError error)
+                { model | newUserForm = ( fst model.newUserForm, Just (decodeError error) ) }
+                    ! []
+                    |> andLog "Create User failed" (toString <| decodeError error)

         DeleteUser user ->
             model ! [ API.deleteUser model user (UserMsg' << DeleteUserFailed) (UserMsg' << DeleteUserSucceeded) ]
@@ -220,12 +223,18 @@ updateUserMsg msg model =
                     { model | shownUser = Nothing } ! [ Navigation.newUrl <| Route.urlFor <| Route.ShowUser id ]

         NewUserFormMsg formMsg ->
-            case ( formMsg, Form.getOutput model.newUserForm ) of
+            case ( formMsg, Form.getOutput (fst model.newUserForm) ) of
                 ( Form.Submit, Just user ) ->
                     model ! [ API.createUser model user (UserMsg' << CreateUserFailed) (UserMsg' << CreateUserSucceeded) ]

                 _ ->
-                    { model | newUserForm = Form.update formMsg model.newUserForm } ! []
+                    { model
+                        | newUserForm =
+                            ( Form.update formMsg (fst model.newUserForm)
+                            , snd model.newUserForm
+                            )
+                    }
+                        ! []

Include the server-side errors in the view

This is where the squirrelly UX-logic resides, unsurprisingly.

 updateProjectMsg : ProjectMsg -> Model -> ( Model, Cmd Msg )
diff --git a/elm/src/View/Users/New.elm b/elm/src/View/Users/New.elm
index 3cdcb3d..a367402 100644
--- a/elm/src/View/Users/New.elm
+++ b/elm/src/View/Users/New.elm
@@ -17,6 +17,8 @@ import Form exposing (Form)
 import Form.Field
 import Form.Input
 import Form.Error
+import String
+import Dict


 view : Model -> Html Msg
@@ -31,41 +33,42 @@ view model =
         ]


-
---Html.App.map (UserMsg' << NewUserFormMsg) <| viewForm newUserForm
--- viewForm : Form () User -> Html Form.Msg
--- viewForm form =
---     let
---         -- error presenter
---         errorFor field =
---             case field.liveError of
---                 Just error ->
---                     -- replace toString with your own translations
---                     div [ class "error" ] [ text (toString error) ]
---
---                 Nothing ->
---                     text ""
---
---         -- fields states
---         name =
---             Form.getFieldAsString "name" form
---     in
---         div []
---             [ label [] [ text "Name" ]
---             , Input.textInput name []
---             , errorFor name
---             , button [ onClick Form.Submit ]
---                 [ text "Submit" ]
---             ]
---
-
-
 nameField : Model -> Html Msg
 nameField model =
     let
+        rawName =
+            Form.getFieldAsString "name" (fst model.newUserForm)
+                |> Debug.log "rawName"
+
+        apiErrors =
+            (snd model.newUserForm)
+
         name =
-            Form.getFieldAsString "name" model.newUserForm
+            case apiErrors of
+                Nothing ->
+                    rawName

+                Just errorDict ->
+                    case ( rawName.isDirty, rawName.liveError /= Nothing ) of
+                        ( True, True ) ->
+                            rawName
+
+                        ( False, True ) ->
+                            rawName
+
+                        ( True, False ) ->
+                            rawName
+
+                        ( _, False ) ->
+                            case Dict.get "name" errorDict of
+                                Nothing ->
+                                    rawName
+
+                                Just errorList ->
+                                    { rawName | liveError = Just <| Form.Error.CustomError (String.join ", " errorList) }
+
+        -- We have an api field error thing
+        -- we want to be able to merge those here
         conditionalProperties =
             case name.liveError of
                 Just error ->
@@ -76,6 +79,9 @@ nameField model =
                         Form.Error.Empty ->
                             [ Textfield.error "Cannot be blank" ]

+                        Form.Error.CustomError errString ->
+                            [ Textfield.error errString ]
+
                         _ ->
                             [ Textfield.error <| toString error ]

Add uniqueness on users' names to the Phoenix API.


diff --git a/time_tracker_backend/priv/repo/migrations/20160908151941_add_uniqueness_constraint_to_users_name.exs b/time_tracker_backend/priv/repo/migrations/20160908151941_add_uniqueness_constraint_to_users_name.exs
new file mode 100644
index 0000000..182c214
--- /dev/null
+++ b/time_tracker_backend/priv/repo/migrations/20160908151941_add_uniqueness_constraint_to_users_name.exs
@@ -0,0 +1,10 @@
+defmodule TimeTrackerBackend.Repo.Migrations.AddUniquenessConstraintToUsersName do
+  use Ecto.Migration
+
+  def change do
+    alter table(:users) do
+      modify :name, :string, unique: true
+    end
+    create unique_index(:users, [:name])
+  end
+end
diff --git a/time_tracker_backend/test/controllers/user_controller_test.exs b/time_tracker_backend/test/controllers/user_controller_test.exs
index 8981d0f..b47518c 100644
--- a/time_tracker_backend/test/controllers/user_controller_test.exs
+++ b/time_tracker_backend/test/controllers/user_controller_test.exs
@@ -37,6 +37,12 @@ defmodule TimeTrackerBackend.UserControllerTest do
     assert json_response(conn, 422)["errors"] != %{}
   end

+  # test "does not create resource and renders errors when username already exists", %{conn: conn} do
+  #   %User{name: "foo"} |> Repo.insert!
+  #   conn = post conn, user_path(conn, :create), user: %{"name" => "foo"}
+  #   assert json_response(conn, 422)["errors"] != %{"name" => ["already exists"]}
+  # end
+
   test "updates and renders chosen resource when data is valid", %{conn: conn} do
     user = Repo.insert! %User{}
     conn = put conn, user_path(conn, :update, user), user: @valid_attrs
diff --git a/time_tracker_backend/test/models/user_test.exs b/time_tracker_backend/test/models/user_test.exs
new file mode 100644
index 0000000..40de70f
--- /dev/null
+++ b/time_tracker_backend/test/models/user_test.exs
@@ -0,0 +1,23 @@
+defmodule TimeTrackerBackend.UserTest do
+  use TimeTrackerBackend.ModelCase
+
+  alias TimeTrackerBackend.{User, Repo}
+
+  @valid_attrs %{name: "some content"}
+  @invalid_attrs %{}
+
+  test "changeset with valid attributes" do
+    changeset = User.changeset(%User{}, @valid_attrs)
+    assert changeset.valid?
+  end
+
+  test "changeset with invalid attributes" do
+    changeset = User.changeset(%User{}, @invalid_attrs)
+    refute changeset.valid?
+  end
+
+  test "names are unique" do
+    IO.inspect User.changeset(%User{}, @valid_attrs) |> Repo.insert!
+    IO.inspect User.changeset(%User{}, @valid_attrs) |> Repo.insert!
+  end
+end
diff --git a/time_tracker_backend/web/models/user.ex b/time_tracker_backend/web/models/user.ex
index c5682f1..ab08fd8 100644
--- a/time_tracker_backend/web/models/user.ex
+++ b/time_tracker_backend/web/models/user.ex
@@ -14,5 +14,6 @@ defmodule TimeTrackerBackend.User do
     struct
     |> cast(params, [:name])
     |> validate_required([:name])
+    |> unique_constraint(:name)
   end
 end

Summary

OK, so this was not a ton of code to introduce to get a very nice client-and-server-side validation story, but it did take quite a bit of time pairing with Luke to get it nice. I hope you enjoyed it. See you soon!

Resources