Using Haskell's Text.ParserCombinators.ReadP library for the first time, and I
enjoyed it thoroughly! It's nice avoiding a third-party library like MegaParsec.
Before starting my take-home assignment, the instructions advised me to create a
"Hello, world" program in the language of my choice. Since I'm choosing Haskell,
I created this example as my starter boilerplate.
I think setting -Wall is a sensible default and @dmjio confirmed this. After
putting this in my project's .ghci file, a few dozen warnings emerged. This
commit changes the code that causes the warnings.
Create a project-local .ghci file to define sensible
defaults (e.g. -Wincomplete-patterns).
TODO: Discover more GHC options to put in this file.
I would prefer to keep this at the project root, but because I'm running the
project from the src directory, I need to keep .ghci there.
Two things:
1. I've never attempted to support this before.
2. It seems surprisingly and perhaps deceptively simpler than what I
expected. I'm unsure what to do once Google's API authenticates the user. I
currently look-up the user's role, trips, etc. using their email address. The
role is stored in the Accounts table alongside username, email, password. I
will speak with the interviewer tomorrow about this.
TL;DR: My trips.csv had invalid dates for the endDate column. "2020-15-30" is an
"invalid date" (according to FromField instance for Calendar.Day) bc 15 is not a
valid month (i.e. [1,12]).
@dmjio helped me take a look. When we poked around the SQL, we discovered:
```sql
SELECT endDate FROM TRIPS; -- shows three records
SELECT date(endDate) FROM TRIPS; -- shows two records
```
Copy Example:
To accept the invitation:
POST /accept-invitation username=<username>
password=<password> email=you@domain.tld
secret=8c6b5719-7b1c-471c-bdea-7807b6c0866c
Allow users to accept invitations that we email to them.
TL;DR:
- I learned how to write FromHttpApiData instances, which allows me to
parse/validate data at the edges of my application; this substantially cleans
up my Handler code.
TL;DR:
- Prefer the more precise verbiage, "Accounts", to "Users"
- Add username field to Trip instead of relying on session.username
- Ensure that decodeRole can JD.fail for invalid inputs
- Define print.css with media=print type (note: could've been handled with
@media queries)
- Define printPage port to interop with window.print() JS function
- Support UI.wrapNoPrint to wrap components with a the no-print CSS
TL;DR:
- Ensure Types.TripPK in Types.hs uses Calendar.Day for startDate
- Prefer verbiage "GotCreateTrip" to "CreatedTrip"
- Extend Utils.deleteWithCredentials to accept a body parameter
- Support a delete button in the UI
Problem: When I'm working on a feature, I save my code, and elm-live reloads the
browser. This is usually good, except that the application state is
reinitialized, which usually means that the view changes.
I defined two state configurations, and I expect to define more:
- prod: The initial state for the application
- userHome: The state I'd like to use when developing a feature for the UserHome
page.
Idea: For more ad-hoc configurations, I can store the application state in
LocalStorage and restore it in between page refreshes.
*sigh* ... spent way too much time encoding/decoding date types...
I need my database, server, client, and JSON need to agree on types.
TL;DR:
- Add CSS for elm/datepicker library
- Create Common.allErrors to display UI errors
- Prefer Data.Time.Calendar.Day instead of newtype Date wrapper around Text
Client-side, I'm not exposing the role option to users. Server-side, I'm
asserting that requests to create Manager and Admin accounts are attempted by
users with a session tied to an admin account.
Toggle b/w logging in or signing up.
TL;DR:
- From my previous submission's feedback, disallow users from signing themselves
up as admins, managers; I just removed the UI element altogether, even though
the server still support this (TODO)
@dmjio says (probably correctly) that it's best to just serve the client from
the server and circumvent CORS issues altogether.
One day I will set that up. For now, this works... *sigh*
Support a top-level PATCH request to trips that permits any admin to update any
trip, and any user to update any of their trips.
I'm using Aeson's (:?) combinator to support missing fields from the incoming
JSON requests, and then M.fromMaybe to apply these values to any record that
matches the primary key.
See the TODOs that I introduced for some shortcomings.
Somebody incremenet the total number of off-by-one errors that I've made in my
career. I think the current count is 99... or is it 100? 101? Who knows?!
When this was an UPDATE statement with a WHERE clause, and the LoginAttempts
table was vacant, nothing would happen. Thankfully, SQLite supports an UPSERT
clause so that I can INSERT a new record or UPDATE conditionally.
And the best part is: it works!
"SELECT *" in SQL may not guarantee the order in which a record's columns are
returned. For example, in my FromRow instances for Account, I make successive call
The following scenario silently and erroneously assigns:
firstName, lastName = lastName, firstName
```sql
CREATE TABLE People (
firstName TEXT NOT NULL,
lastName TEXT NOT NULL,
age INTEGER NOT NULL,
PRIMARY KEY (firstName, lastName)
)
```
```haskell
data Person = Person { firstName :: String, lastName :: String, age :: Integer }
fromRow = do
firstName <- field
lastName <- field
age <- field
pure Person{..}
getPeople :: Connection -> IO [Person]
getPeople conn = query conn "SELECT * FROM People"
```
This silently fails because both firstName and lastName are Strings, and so the
FromRow Person instance type-checks, but you should expect to receive a list of
names like "Wallace William" instead of "William Wallace".
The following won't break the type-checker, but will result in a runtime parsing
error:
```haskell
-- all code from the previous example remains the same except for:
fromRow = do
age <- field
firstName <- field
lastName <- field
```
The "SELECT *" will return records like (firstName,lastName,age), but the
FromRow instance for Person will attempt to parse firstName as
Integer.
So... what have we learned? Prefer "SELECT (firstName,lastName,age)" instead of
"SELECT *".
Lots of changes here:
- Add the GET /verify endpoint
- Email users a secret using MailGun
- Create a PendingAccounts table and record type
- Prefer do-notation for FromRow instances (and in general) instead of the <*>
or a liftA2 style. Using instances using `<*>` makes the instances depend on
the order in which the record's fields were defined. When combined with a
"SELECT *", which returns the columns in whichever order the schema defines
them (or depending on the DB implementation), produces runtime parse errors
at best and silent errors at worst.
- Delete bill from accounts.csv to free up the wpcarro@gmail.com when testing
the /verify route.
Whichever package is on nixpkgs right now is broken, so I'm using `fetchGit` and
`callCabal2nix`.
Create Email module exposing a simplifies `send` function that partially applies
some of the configuration options.
Using my dear friend's, dmjio's, excellent library, envy -- to read and parse
variables from the system environment.
I added and git-ignored the .envrc file that contains API secrets. I'm using
Envy to read these values, so that I don't hard-code these values into the
source code.
If I ever fully learn `servant-auth`, I'll probably recognize how naive this
hand-rolled solution is. But it works! And the code is pretty declarative, which
I like.