Skip to content

Commit

Permalink
Merge pull request #5 from eldr-io/feature/folder-restructure
Browse files Browse the repository at this point in the history
Rename folder to "src" and add UUID Id documentation recipe
  • Loading branch information
sigrdrifa authored Oct 8, 2024
2 parents 956309e + 2a05b99 commit 96498f2
Show file tree
Hide file tree
Showing 21 changed files with 110 additions and 9 deletions.
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
help: ## print make targets
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'

.PHONY: ghcid-devel
ghcid-devel: tailwind-build ## Run the server in fast development mode. See lib/DevelMain.hs for details.
.PHONY: watch
watch: tailwind-build ## Run the server in fast development mode. See lib/DevelMain.hs for details.
ghcid \
--command "cabal repl hastl" \
--test DevelMain.update \
Expand Down
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,10 @@ This will build and run hastl and you should be able to navigate to `localhost:8

![Screenshot from 2024-06-08 15-41-54](https://github.com/eldr-io/hastl/assets/83576392/19af0d8e-33b8-411e-a19e-e2e4f8c3420f)

To run the development live reloading mode, make sure that <a href="https://github.com/ndmitchell/ghcid">ghcid</a> is installed and then use the ghcid-devel target:
To run the development live reloading mode, make sure that <a href="https://github.com/ndmitchell/ghcid">ghcid</a> is installed and then use the watch target:

```
make ghcid-devel
make watch
```

#### Running unit tests
Expand Down Expand Up @@ -84,6 +84,10 @@ Hastl allows you to combine strongly-typed Servant APIs to make up your applicat

If you wish to use persistent models in your application, you can define your models in `lib/Models.hs` and persistent will automatically create the Haskell types, as well as handling the database migrations for DEVELOPMENT setups (note: is it recommended to use a more robust migration mechanism for production).

#### Documentation Recipes

- [Using UUIDs instead of Integer IDs as Primary Keys in database](https://github.com/eldr-io/hastl/blob/main/docs/recipes/using-uuids-for-db-ids.md)

[Haskell]: https://img.shields.io/badge/haskell-5D4F85?style=for-the-badge&logo=haskell&logoColor=white
[Haskell-url]: https://haskell.org
[Htmx]: https://img.shields.io/badge/htmxjs-3366CC?style=for-the-badge&logo=htmx&logoColor=white
Expand Down
97 changes: 97 additions & 0 deletions docs/recipes/using-uuids-for-db-ids.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
## Using UUIDs as Database Primary Key Id fields

You may want to switch Hastl to use unique UUIDs as the id for all of the database models, rather than the standard BIGSERIAL that is just an incremental number.

To accomplish this in hastl, you can use the "implicitIdDef" functionality of persistent.

To get started, create a new UUID type that will be our custom UUID implementation that persistent will map our id fields to, e.g. `src/Api/UUID.hs`:

```hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}

module Api.UUID where

import Data.Aeson (FromJSON (parseJSON), ToJSON (toJSON))
import Data.ByteString (ByteString)
import Data.ByteString.Char8 qualified as B8
import Data.Text qualified as T
import Database.Persist (LiteralType (Escaped), PersistField (fromPersistValue, toPersistValue), PersistValue (PersistLiteral_), SqlType (SqlOther))
import Database.Persist.ImplicitIdDef (ImplicitIdDef, mkImplicitIdDef)
import Database.Persist.Postgresql (PersistFieldSql (sqlType))
import Servant qualified as Web.Internal
import Web.PathPieces

newtype UUID = UUID ByteString

instance ToJSON UUID where
toJSON (UUID bs) = toJSON (B8.unpack bs)

instance FromJSON UUID where
parseJSON v = UUID . B8.pack <$> parseJSON v

instance Show UUID where
show (UUID bs) = show bs

instance Read UUID where
readsPrec _ s = [(UUID (read s), "")]

instance Eq UUID where
(UUID a) == (UUID b) = a == b

instance Ord UUID where
compare (UUID a) (UUID b) = compare a b

fromByteString :: ByteString -> Maybe UUID
fromByteString bs = Just (UUID bs)

instance Web.PathPieces.PathPiece UUID where
toPathPiece (UUID bs) = T.pack $ B8.unpack bs
fromPathPiece s = Just (UUID (B8.pack (T.unpack s)))

instance Web.Internal.ToHttpApiData UUID where
toUrlPiece (UUID bs) = T.pack $ B8.unpack bs

instance Web.Internal.FromHttpApiData UUID where
parseUrlPiece s = Right (UUID (B8.pack (T.unpack s)))

instance PersistField UUID where
toPersistValue (UUID bs) =
PersistLiteral_ Escaped bs
fromPersistValue pv =
case pv of
PersistLiteral_ Escaped bs ->
Right (UUID bs)
_ ->
Left "nope"

instance PersistFieldSql UUID where
sqlType _ = SqlOther "UUID"

uuidDef :: ImplicitIdDef
uuidDef = mkImplicitIdDef @UUID "gen_random_uuid()"
```
This defines several helpful instances for our custom UUID type, and importantly also maps it to the database function "gen_random_uuid()" which will cause
Postgres to automatically generate a new UUID for us on insertion.

With the new UUID field defined we can now add the setImplicitDef macro, referencing the uuidDef from `src/Api/UUID.hs` to our models persistent block in `src/Models.hs`:

```hs
share
[ mkPersist (setImplicitIdDef uuidDef sqlSettings)
, mkMigrate "migrateAll"
]
[persistLowerCase|
Guest json
name Text
email Text
...
```
and now if we re-run our migrations (note that persistent may not be able to automatically change BigSerial Int ids to this new type), hastl should now be using UUIDs for all Ids.
2 changes: 1 addition & 1 deletion hastl.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ library
safe >= 0.3.21 && < 0.4,
say >= 0.1.0 && < 0.2,
wai-middleware-metrics >= 0.2.4 && < 0.3,
hs-source-dirs: lib
hs-source-dirs: src
default-language: GHC2021

executable hastl
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ renderHome =
h1_ [class_ "text-3xl font-bold text-gray-900"] "Welcome to hastl"
p_ [class_ "text-gray-600"] $ do
"hastl is a modern "
a_ [href_ "haskell.org", target_ "_blank"] "Haskell"
a_ [href_ "https://haskell.org", target_ "_blank"] "Haskell"
" web application using "
b_ "(H)tmx, "
b_ "(A)lpine.js, "
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
2 changes: 1 addition & 1 deletion lib/Config.hs → src/Config.hs
Original file line number Diff line number Diff line change
Expand Up @@ -154,4 +154,4 @@ envPool Production = 8
@""@ for 'Development' or @"test"@ for 'Test'.
-}
connStr :: BS.ByteString -> ConnectionString
connStr sfx = "host=localhost dbname=postgres" <> sfx <> " user=postgres password=postgres port=5432"
connStr sfx = "host=localhost dbname=postgres" <> sfx <> " user=postgres password=test port=5432"
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
4 changes: 2 additions & 2 deletions tailwind.config.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/** @type {import('tailwindcss').Config} */
export const content = [
"./lib/Api/Templates/**/*.hs",
"./lib/Api/*.hs",
"./src/Api/Templates/**/*.hs",
"./src/Api/*.hs",
];
export const theme = {
container: {
Expand Down

0 comments on commit 96498f2

Please sign in to comment.