diff --git a/Makefile b/Makefile index 088fd4d..c0a4272 100644 --- a/Makefile +++ b/Makefile @@ -7,11 +7,6 @@ dist/test: dist/app gren_packages: gren.json gren package install -.PHONY: db -db: - # TODO: get db url from env - ws4sql --quick-db=db/local.db - .PHONY: server server: dist/app node dist/app diff --git a/devbox.json b/devbox.json index 7b5b001..be4d95c 100644 --- a/devbox.json +++ b/devbox.json @@ -1,11 +1,10 @@ { "$schema": "https://raw.githubusercontent.com/jetify-com/devbox/0.13.6/.schema/devbox.schema.json", "packages": [ - "nodejs@20", - "gren@0.6.3", + "nodejs@24", + "gren@0.6.5", "curl@latest", "jq@latest", - "github:blaix/ws4sql-nix/0.17dev7", "sqlite@latest" ], "shell": { diff --git a/devbox.lock b/devbox.lock index ed46df5..0f8f499 100644 --- a/devbox.lock +++ b/devbox.lock @@ -128,55 +128,51 @@ "github:NixOS/nixpkgs/nixpkgs-unstable": { "resolved": "github:NixOS/nixpkgs/3549532663732bfd89993204d40543e9edaec4f2?lastModified=1742272065&narHash=sha256-ud8vcSzJsZ%2FCK%2Br8%2Fv0lyf4yUntVmDq6Z0A41ODfWbE%3D" }, - "github:blaix/ws4sql-nix/0.17dev7": { - "last_modified": "2026-03-11T20:58:00Z", - "resolved": "github:blaix/ws4sql-nix/4837de181c857cdb9fa015f7fb9a018cceec8532?lastModified=1773262680&narHash=sha256-mOBI%2BWUDHjd6xf3JhlC%2BMkt2Nk8BzN20J0dZfQetHbc%3D" - }, - "gren@0.6.3": { - "last_modified": "2025-09-18T16:33:27Z", - "resolved": "github:NixOS/nixpkgs/f4b140d5b253f5e2a1ff4e5506edbf8267724bde#gren", + "gren@0.6.5": { + "last_modified": "2026-04-16T08:46:55Z", + "resolved": "github:NixOS/nixpkgs/b86751bc4085f48661017fa226dee99fab6c651b#gren", "source": "devbox-search", - "version": "0.6.3", + "version": "0.6.5", "systems": { "aarch64-darwin": { "outputs": [ { "name": "out", - "path": "/nix/store/clr249d9d9rlqs3fsgj2lb4m97ph2hms-gren-0.6.3", + "path": "/nix/store/kijfkwvkxkfp8vpzj630q99k5cymdgx2-gren-0.6.5", "default": true } ], - "store_path": "/nix/store/clr249d9d9rlqs3fsgj2lb4m97ph2hms-gren-0.6.3" + "store_path": "/nix/store/kijfkwvkxkfp8vpzj630q99k5cymdgx2-gren-0.6.5" }, "aarch64-linux": { "outputs": [ { "name": "out", - "path": "/nix/store/zgx86f6s63l0ckj78psmkdkwqfxr3770-gren-0.6.3", + "path": "/nix/store/a2za3hav09hg1amcj225b913dyfjs2wp-gren-0.6.5", "default": true } ], - "store_path": "/nix/store/zgx86f6s63l0ckj78psmkdkwqfxr3770-gren-0.6.3" + "store_path": "/nix/store/a2za3hav09hg1amcj225b913dyfjs2wp-gren-0.6.5" }, "x86_64-darwin": { "outputs": [ { "name": "out", - "path": "/nix/store/1gdj8kkrb5d945rklcxwqzl8s55kjpyx-gren-0.6.3", + "path": "/nix/store/0mrp2wghqzmlcqc6nrfpv9l7s1p3kmcg-gren-0.6.5", "default": true } ], - "store_path": "/nix/store/1gdj8kkrb5d945rklcxwqzl8s55kjpyx-gren-0.6.3" + "store_path": "/nix/store/0mrp2wghqzmlcqc6nrfpv9l7s1p3kmcg-gren-0.6.5" }, "x86_64-linux": { "outputs": [ { "name": "out", - "path": "/nix/store/xhc5c5vl8jxxm0jrr8vap34agz3f6yfi-gren-0.6.3", + "path": "/nix/store/3bc06xrds99f1cdhymrfyv8nwi43q8bp-gren-0.6.5", "default": true } ], - "store_path": "/nix/store/xhc5c5vl8jxxm0jrr8vap34agz3f6yfi-gren-0.6.3" + "store_path": "/nix/store/3bc06xrds99f1cdhymrfyv8nwi43q8bp-gren-0.6.5" } } }, @@ -296,68 +292,84 @@ } } }, - "nodejs@20": { - "last_modified": "2025-02-12T00:10:52Z", + "nodejs@24": { + "last_modified": "2025-12-29T16:45:58Z", "plugin_version": "0.0.2", - "resolved": "github:NixOS/nixpkgs/83a2581c81ff5b06f7c1a4e7cc736a455dfcf7b4#nodejs_20", + "resolved": "github:NixOS/nixpkgs/346dd96ad74dc4457a9db9de4f4f57dab2e5731d#nodejs_24", "source": "devbox-search", - "version": "20.18.3", + "version": "24.12.0", "systems": { "aarch64-darwin": { "outputs": [ { "name": "out", - "path": "/nix/store/pba611q472k53hjk8kai3mqg5nqa0q4v-nodejs-20.18.3", + "path": "/nix/store/b1h0af3yb3z9jz048phclcqw6ihiww67-nodejs-24.12.0", "default": true }, + { + "name": "dev", + "path": "/nix/store/ksn4nj0nbp0ikmh4scy9x4biqqdswwhy-nodejs-24.12.0-dev" + }, { "name": "libv8", - "path": "/nix/store/giz92jzgk5j5x4rr87qfhh7h23vxw02s-nodejs-20.18.3-libv8" + "path": "/nix/store/3x72bm7l30ib7l7cximrp0sklcq8jqwy-nodejs-24.12.0-libv8" } ], - "store_path": "/nix/store/pba611q472k53hjk8kai3mqg5nqa0q4v-nodejs-20.18.3" + "store_path": "/nix/store/b1h0af3yb3z9jz048phclcqw6ihiww67-nodejs-24.12.0" }, "aarch64-linux": { "outputs": [ { "name": "out", - "path": "/nix/store/958yayymdnm342x6vldlfbjah51dy0d4-nodejs-20.18.3", + "path": "/nix/store/9jfsyhcr9c6rpm94lrkibv75jkggvwss-nodejs-24.12.0", "default": true }, + { + "name": "dev", + "path": "/nix/store/7izhv9x1yvsmyrhmmyps6bxjjclajm80-nodejs-24.12.0-dev" + }, { "name": "libv8", - "path": "/nix/store/wxl92b7ld4dfhskvmgvf9r7q9h86q2s6-nodejs-20.18.3-libv8" + "path": "/nix/store/8hl74pnc1qyz68rg10ixw7fz32hcxqkk-nodejs-24.12.0-libv8" } ], - "store_path": "/nix/store/958yayymdnm342x6vldlfbjah51dy0d4-nodejs-20.18.3" + "store_path": "/nix/store/9jfsyhcr9c6rpm94lrkibv75jkggvwss-nodejs-24.12.0" }, "x86_64-darwin": { "outputs": [ { "name": "out", - "path": "/nix/store/3wrrj4ddsc8mrim3xnqwws8w2b3an4jv-nodejs-20.18.3", + "path": "/nix/store/cj1243xswxvnwgifyiyg8axhn0r2vl48-nodejs-24.12.0", "default": true }, + { + "name": "dev", + "path": "/nix/store/kzia3rkaf9vwpn3py23q9v49jpjm6zx7-nodejs-24.12.0-dev" + }, { "name": "libv8", - "path": "/nix/store/bw62bzsr6rgrygpk5g4vjpbbqwffn8sg-nodejs-20.18.3-libv8" + "path": "/nix/store/xszji81xj7ghjhz7jk0jkhb84nfwyd8f-nodejs-24.12.0-libv8" } ], - "store_path": "/nix/store/3wrrj4ddsc8mrim3xnqwws8w2b3an4jv-nodejs-20.18.3" + "store_path": "/nix/store/cj1243xswxvnwgifyiyg8axhn0r2vl48-nodejs-24.12.0" }, "x86_64-linux": { "outputs": [ { "name": "out", - "path": "/nix/store/1bmfcixbhs5fxv2zzyc45grx3xgc90j9-nodejs-20.18.3", + "path": "/nix/store/9z1v3wyrxp6fpyzw21lcakd5w7aknzyc-nodejs-24.12.0", "default": true }, { "name": "libv8", - "path": "/nix/store/03gwhdryzihhv729133pfmri9qjhnbr5-nodejs-20.18.3-libv8" + "path": "/nix/store/5yvx909wlis13qd2y1ahwl7ij21ma69a-nodejs-24.12.0-libv8" + }, + { + "name": "dev", + "path": "/nix/store/7ll99p08gmpg0n048669p80zjibl5akr-nodejs-24.12.0-dev" } ], - "store_path": "/nix/store/1bmfcixbhs5fxv2zzyc45grx3xgc90j9-nodejs-20.18.3" + "store_path": "/nix/store/9z1v3wyrxp6fpyzw21lcakd5w7aknzyc-nodejs-24.12.0" } } }, diff --git a/gren.json b/gren.json index 2a64fa2..582e016 100644 --- a/gren.json +++ b/gren.json @@ -4,13 +4,12 @@ "source-directories": [ "src" ], - "gren-version": "0.6.3", + "gren-version": "0.6.5", "dependencies": { "direct": { "blaix/gren-effectful-tests": "6.0.0", - "blaix/gren-ws4sql": "4.0.0", "gren-lang/core": "7.0.0", - "gren-lang/node": "6.0.0", + "gren-lang/node": "local:../node", "gren-lang/test": "5.0.0", "gren-lang/url": "6.0.0" }, diff --git a/gren_packages/blaix_gren_ws4sql__4_0_0.pkg.gz b/gren_packages/blaix_gren_ws4sql__4_0_0.pkg.gz deleted file mode 100644 index 8e67c51..0000000 Binary files a/gren_packages/blaix_gren_ws4sql__4_0_0.pkg.gz and /dev/null differ diff --git a/gren_packages/gren_lang_node__6_0_0.pkg.gz b/gren_packages/gren_lang_node__6_0_0.pkg.gz deleted file mode 100644 index b043fe8..0000000 Binary files a/gren_packages/gren_lang_node__6_0_0.pkg.gz and /dev/null differ diff --git a/process-compose.yml b/process-compose.yml index 6577b82..bf658ca 100644 --- a/process-compose.yml +++ b/process-compose.yml @@ -2,11 +2,5 @@ version: "0.5" processes: - db: - command: make db - ready_log_line: "Web Service listening" server: command: make server - depends_on: - db: - condition: process_log_ready diff --git a/src/Main.gren b/src/Main.gren index fcbbe2f..f518976 100644 --- a/src/Main.gren +++ b/src/Main.gren @@ -2,9 +2,9 @@ module Main exposing (main) import Bytes exposing (Bytes) import Crypto -import Db +import Sqlite import Dict -import Db.Encode +import FileSystem import HttpClient import HttpServer exposing (Request, ServerError(..), Method(..)) import HttpServer.Response as Response exposing (Response) @@ -18,8 +18,9 @@ import Route.Error import Route.PublishingIdentity import Route.Session import Stream +import Sqlite +import Sqlite.Encode import Task exposing (Task) -import Test.E2E.Helper as Helper import Time import Url import User exposing (User) @@ -48,7 +49,7 @@ type alias Model = { stdout : Stream.Writable Bytes , stderr : Stream.Writable Bytes , server : Maybe HttpServer.Server - , db : Db.Connection + , db : Maybe Sqlite.Database , postmark : Maybe Postmark.Configuration , secureContext : Maybe Crypto.SecureContext } @@ -58,11 +59,9 @@ init : Environment -> Init.Task { model : Model, command : Cmd Msg } init env = Init.await HttpServer.initialize <| \serverPermission -> Init.await HttpClient.initialize <| \httpPerm -> + Init.await FileSystem.initialize <| \fsPerm -> Init.awaitTask Node.getEnvironmentVariables <| \envVars -> let - db = - Helper.initDb httpPerm - postmark = envVars |> Dict.get "POSTMARK_SERVER_TOKEN" @@ -78,14 +77,23 @@ init env = { stdout = env.stdout , stderr = env.stderr , server = Nothing - , db = db + , db = Nothing , secureContext = Nothing , postmark = postmark } , command = + let + initDb = + Registry.Db.open fsPerm + |> Task.andThen + (\db -> + Registry.Db.migrate db + |> Task.map (\_ -> db) + ) + |> Task.attempt DbMigrationResult + in Cmd.batch - [ Registry.Db.migrate db - |> Task.attempt DbMigrationResult + [ initDb , HttpServer.createServer serverPermission serverConfig |> Task.attempt CreateServerResult , Crypto.getSecureContext @@ -99,7 +107,7 @@ init env = type Msg = CreateServerResult (Result HttpServer.ServerError HttpServer.Server) - | DbMigrationResult (Result Db.Error (Array Int)) + | DbMigrationResult (Result Sqlite.Error Sqlite.Database) | SecureContextResult (Result {} Crypto.SecureContext) | GotRequest { request : HttpServer.Request, response : Response } | ResponseReady Route.Response @@ -128,8 +136,8 @@ update msg model = DbMigrationResult result -> when result is - Ok numChanges -> - { model = model + Ok db -> + { model = { model | db = Just db } , command = Cmd.none } @@ -214,10 +222,12 @@ route model request response = config : { secureContext : Maybe Crypto.SecureContext , postmark : Maybe Postmark.Configuration + , db : Maybe Sqlite.Database } config = { secureContext = model.secureContext , postmark = model.postmark + , db = model.db } in when config is @@ -232,7 +242,10 @@ route model request response = { postmark = Nothing } -> Route.Error.serverError response "Missing postmark config." - { postmark = Just postmark, secureContext = Just secureContext } -> + { db = Nothing } -> + Route.Error.serverError response "DB failed to initialize." + + { postmark = Just postmark, secureContext = Just secureContext, db = Just db } -> when { method = request.method, path = path } is @@ -241,7 +254,7 @@ route model request response = { method = POST, path = [ "session" ] } -> Route.Session.create - { db = model.db + { db = db , postmark = postmark , secureContext = secureContext , body = request.body @@ -250,7 +263,7 @@ route model request response = { method = POST, path = [ "session", "fetch" ] } -> Route.Session.fetch - { db = model.db + { db = db , secureContext = secureContext , body = request.body , response = response @@ -262,7 +275,7 @@ route model request response = { method = POST, path = [ "publishing-identity" ] } -> Route.PublishingIdentity.create - { db = model.db + { db = db , body = request.body , response = response } diff --git a/src/PublishingIdentity.gren b/src/PublishingIdentity.gren index 224aff2..e68fc43 100644 --- a/src/PublishingIdentity.gren +++ b/src/PublishingIdentity.gren @@ -5,9 +5,9 @@ module PublishingIdentity exposing ) -import Db -import Db.Decode as Decode -import Db.Encode as Encode +import Sqlite +import Sqlite.Decode as Decode +import Sqlite.Encode as Encode import Task exposing (Task) import User exposing (User) @@ -20,13 +20,13 @@ type alias PublishingIdentity = create : - Db.Connection + Sqlite.Database -> { user : User, name : String } - -> Task Db.Error PublishingIdentity + -> Task Sqlite.Error PublishingIdentity create db { user, name } = let insertIdentity = - Db.execute db + Sqlite.execute db { statement = """ INSERT INTO publishing_identity (name) @@ -38,7 +38,7 @@ create db { user, name } = } getIdentityId = - Db.getOne db + Sqlite.getOne db { query = """ SELECT id FROM publishing_identity @@ -47,11 +47,11 @@ create db { user, name } = , parameters = [ Encode.string "name" name ] - , decoder = Decode.int "id" + , rowDecoder = Decode.int "id" Decode.succeed } insertUserLink identityId = - Db.execute db + Sqlite.execute db { statement = """ INSERT INTO publishing_identity_users (user_id, publishing_identity_id) @@ -77,11 +77,11 @@ create db { user, name } = get : - Db.Connection + Sqlite.Database -> { userId : Int, name : String } - -> Task Db.Error PublishingIdentity + -> Task Sqlite.Error PublishingIdentity get db { userId, name } = - Db.getOne db + Sqlite.getOne db { query = """ SELECT pi.id, pi.name, piu.user_id @@ -95,10 +95,10 @@ get db { userId, name } = [ Encode.int "user_id" userId , Encode.string "name" name ] - , decoder = - Decode.field Decode.int "id" <| \id -> - Decode.field Decode.int "user_id" <| \uid -> - Decode.field Decode.string "name" <| \n -> + , rowDecoder = + Decode.int "id" <| \id -> + Decode.int "user_id" <| \uid -> + Decode.string "name" <| \n -> Decode.succeed { id = id , userId = uid diff --git a/src/Registry/Db.gren b/src/Registry/Db.gren index bd97447..4890f0d 100644 --- a/src/Registry/Db.gren +++ b/src/Registry/Db.gren @@ -1,74 +1,80 @@ module Registry.Db exposing ( migrate + , open ) -import Db +import FileSystem +import FileSystem.Path as Path +import Sqlite import Task exposing (Task) -migrate : Db.Connection -> Task Db.Error (Array Int) -migrate db = - Db.transaction db - [ { statement = "PRAGMA foreign_keys = ON" - , parameters = [] - } +open : FileSystem.Permission -> Task Sqlite.Error Sqlite.Database +open fsPerm = + let + -- TODO: get db path from environment + dbPath = + "./db/registry.db" + in + Sqlite.open fsPerm Sqlite.defaultOptions (Sqlite.File (Path.fromPosixString dbPath)) - , { statement = "PRAGMA busy_timeout = 5000" - , parameters = [] - } - , { statement = - """ - CREATE TABLE IF NOT EXISTS user ( - id INTEGER PRIMARY KEY, - email TEXT NOT NULL UNIQUE, - created INTEGER NOT NULL -- timestamp - ) STRICT - """ - , parameters = [] - } +migrate : Sqlite.Database -> Task Sqlite.Error (Array Sqlite.ExecutionSummary) +migrate db = + [ { statement = + """ + CREATE TABLE IF NOT EXISTS user ( + id INTEGER PRIMARY KEY, + email TEXT NOT NULL UNIQUE, + created INTEGER NOT NULL -- timestamp + ) STRICT + """ + , parameters = [] + } - , { statement = - """ - CREATE TABLE IF NOT EXISTS session ( - id INTEGER PRIMARY KEY, - created INTEGER NOT NULL, - user_id INTEGER NOT NULL REFERENCES user(id), + , { statement = + """ + CREATE TABLE IF NOT EXISTS session ( + id INTEGER PRIMARY KEY, + created INTEGER NOT NULL, + user_id INTEGER NOT NULL REFERENCES user(id) ON DELETE CASCADE, - -- Code emailed to user for email confirmation. - -- User will provide this to the CLI. - email_confirmation_code TEXT NOT NULL UNIQUE, + -- Code emailed to user for email confirmation. + -- User will provide this to the CLI. + email_confirmation_code TEXT NOT NULL UNIQUE, - -- Token used by the CLI to fetch session token. - fetch_session_token TEXT NOT NULL UNIQUE, + -- Token used by the CLI to fetch session token. + fetch_session_token TEXT NOT NULL UNIQUE, - -- Create and return this when CLI posts with email_confirmation_code and fetch_session_token - token TEXT UNIQUE - ) STRICT - """ - , parameters = [] - } + -- Create and return this when CLI posts with email_confirmation_code and fetch_session_token + token TEXT UNIQUE + ) STRICT + """ + , parameters = [] + } - , { statement = - """ - CREATE TABLE IF NOT EXISTS publishing_identity ( - id INTEGER PRIMARY KEY, - name TEXT NOT NULL UNIQUE - ) STRICT - """ - , parameters = [] - } + , { statement = + """ + CREATE TABLE IF NOT EXISTS publishing_identity ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL UNIQUE + ) STRICT + """ + , parameters = [] + } - , { statement = - """ - CREATE TABLE IF NOT EXISTS publishing_identity_users ( - user_id INTEGER NOT NULL REFERENCES user(id), - publishing_identity_id INTEGER NOT NULL REFERENCES publishing_identity(id), - role TEXT, - UNIQUE(user_id, publishing_identity_id) - ) STRICT - """ - , parameters = [] - } - ] + , { statement = + """ + CREATE TABLE IF NOT EXISTS publishing_identity_users ( + user_id INTEGER NOT NULL REFERENCES user(id) ON DELETE CASCADE, + publishing_identity_id INTEGER NOT NULL REFERENCES publishing_identity(id) ON DELETE CASCADE, + role TEXT, + UNIQUE(user_id, publishing_identity_id) + ) STRICT + """ + , parameters = [] + } + ] + |> Sqlite.executeAll db + |> Sqlite.inTransaction db diff --git a/src/Route/PublishingIdentity.gren b/src/Route/PublishingIdentity.gren index dbd2389..3ab9e0b 100644 --- a/src/Route/PublishingIdentity.gren +++ b/src/Route/PublishingIdentity.gren @@ -4,7 +4,7 @@ module Route.PublishingIdentity exposing import Bytes exposing (Bytes) -import Db +import Sqlite import Json.Decode import PublishingIdentity import Route exposing (Response) @@ -17,7 +17,7 @@ import User exposing (User) create : - { db : Db.Connection + { db : Sqlite.Database , body : Bytes , response : Response } diff --git a/src/Route/Session.gren b/src/Route/Session.gren index 36469be..10b6c88 100644 --- a/src/Route/Session.gren +++ b/src/Route/Session.gren @@ -6,7 +6,7 @@ module Route.Session exposing import Bytes exposing (Bytes) import Crypto -import Db +import Sqlite import Email exposing (Email) import HttpClient import HttpServer exposing (Request) @@ -22,7 +22,7 @@ import User exposing (User) type Error - = DbError Db.Error + = DbError Sqlite.Error | SendEmailFailed HttpClient.Error @@ -35,7 +35,7 @@ This will email a confirmation code and return a fetch session token. Both must be POSTED to `fetch` to get a long lived session token. -} create : - { db : Db.Connection + { db : Sqlite.Database , postmark : Postmark.Configuration , secureContext : Crypto.SecureContext , body : Bytes @@ -62,7 +62,7 @@ POST emailConfirmationCode and fetchSessionToken from the `create` call to get a long-lived session token you can store to make authenticated API requests. -} fetch : - { db : Db.Connection + { db : Sqlite.Database , secureContext : Crypto.SecureContext , body : Bytes , response : Response @@ -140,13 +140,13 @@ fetchRequestDecoder = -- ACTIONS -findOrCreateUser : Db.Connection -> Email -> Task Error User +findOrCreateUser : Sqlite.Database -> Email -> Task Error User findOrCreateUser db email = User.findOrCreate db email |> Task.mapError DbError -createSession : Db.Connection -> Crypto.SecureContext -> User -> Task Error Session +createSession : Sqlite.Database -> Crypto.SecureContext -> User -> Task Error Session createSession db secureContext user = Session.create { db = db, secureContext = secureContext, user = user } |> Task.mapError DbError @@ -189,7 +189,7 @@ createFailed response error = let errorMsg = "Database error creating session: " ++ - Db.errorToString e + Sqlite.errorToString e in response |> Route.log (Log.Error errorMsg) @@ -229,7 +229,7 @@ fetchSuccess response sessionToken = fetchFailed : Response -> Error -> Task x Response fetchFailed response error = when error is - DbError Db.NoResultError -> + DbError Sqlite.NoResultsError -> response |> Route.setStatus 400 |> setErrorMessage "Invalid token or confirmation code" @@ -239,7 +239,7 @@ fetchFailed response error = let errorMsg = "Error fetching session from DB: " ++ - Db.errorToString e + Sqlite.errorToString e in response |> Route.log (Log.Error errorMsg) diff --git a/src/Session.gren b/src/Session.gren index 543f821..2a39efb 100644 --- a/src/Session.gren +++ b/src/Session.gren @@ -7,9 +7,9 @@ module Session exposing import Bytes.Decode import Crypto -import Db -import Db.Encode -import Db.Decode +import Sqlite +import Sqlite.Encode as Encode +import Sqlite.Decode as Decode import Email import Math import Task exposing (Task) @@ -32,11 +32,11 @@ type alias Session = create : - { db : Db.Connection + { db : Sqlite.Database , secureContext : Crypto.SecureContext , user : User } - -> Task Db.Error Session + -> Task Sqlite.Error Session create { db, user, secureContext } = Task.await (generateEmailConfirmationCode secureContext) <| \code -> Task.await (Crypto.randomUuidV4 secureContext) <| \uuid2 -> @@ -75,16 +75,16 @@ step. Creates and returns the long-lived session token. To keep fetchSessionToken single-use, this will error if session token already exists. -} fetch : - { db : Db.Connection + { db : Sqlite.Database , secureContext : Crypto.SecureContext , fetchSessionToken : String , emailConfirmationCode : String } - -> Task Db.Error String + -> Task Sqlite.Error String fetch { db, secureContext, fetchSessionToken, emailConfirmationCode } = let getSession cutoff = - Db.getOne db + Sqlite.getOne db { query = """ select id from session @@ -94,16 +94,16 @@ fetch { db, secureContext, fetchSessionToken, emailConfirmationCode } = and token is null """ , parameters = - [ Db.Encode.string "fetch_session_token" fetchSessionToken - , Db.Encode.string "email_confirmation_code" emailConfirmationCode - , Db.Encode.posix "cutoff" cutoff + [ Encode.string "fetch_session_token" fetchSessionToken + , Encode.string "email_confirmation_code" emailConfirmationCode + , Encode.time "cutoff" cutoff ] - , decoder = - Db.Decode.int "id" + , rowDecoder = + Decode.int "id" Decode.succeed } setSessionToken id sessionToken = - Db.execute db + Sqlite.execute db { statement = """ update session @@ -111,8 +111,8 @@ fetch { db, secureContext, fetchSessionToken, emailConfirmationCode } = where id = :id """ , parameters = - [ Db.Encode.string "token" sessionToken - , Db.Encode.int "id" id + [ Encode.string "token" sessionToken + , Encode.int "id" id ] } in @@ -131,19 +131,19 @@ fetch { db, secureContext, fetchSessionToken, emailConfirmationCode } = -- DB HELPERS -dbInsert : Db.Connection -> Session -> Task Db.Error Session +dbInsert : Sqlite.Database -> Session -> Task Sqlite.Error Session dbInsert db session = Task.map (\_ -> session) <| - Db.execute db + Sqlite.execute db { statement = """ insert into session (created, user_id, email_confirmation_code, fetch_session_token) values (:created, :user_id, :email_confirmation_code, :fetch_session_token) """ , parameters = - [ Db.Encode.posix "created" session.created - , Db.Encode.int "user_id" session.user.id - , Db.Encode.string "email_confirmation_code" session.emailConfirmationCode - , Db.Encode.string "fetch_session_token" session.fetchSessionToken + [ Encode.time "created" session.created + , Encode.int "user_id" session.user.id + , Encode.string "email_confirmation_code" session.emailConfirmationCode + , Encode.string "fetch_session_token" session.fetchSessionToken ] } diff --git a/src/Test/E2E.gren b/src/Test/E2E.gren index 2d3ca05..fbe384b 100644 --- a/src/Test/E2E.gren +++ b/src/Test/E2E.gren @@ -1,14 +1,13 @@ module Test.E2E exposing (tests) import Crypto -import Db +import Sqlite import Dict import Expect import HttpClient -import Node import Task import Test.Runner.Effectful exposing (Test, await, awaitError, concat, describe, test) -import Test.E2E.Helper exposing (get, expectBadStatus, initDb) +import Test.E2E.Helper exposing (get, expectBadStatus) import Test.E2E.Route.Session import Test.E2E.Route.PublishingIdentity import Test.E2E.Postmark @@ -17,13 +16,13 @@ import Test.E2E.Session import Test.E2E.User -tests : HttpClient.Permission -> Array Test -tests httpPerm = +tests : + { httpPerm : HttpClient.Permission + , db : Sqlite.Database + } + -> Array Test +tests { httpPerm, db } = let - db : Db.Connection - db = - initDb httpPerm - postmark = -- https://postmarkapp.com/support/article/1213-best-practices-for-testing-your-emails-through-postmark { apiToken = "POSTMARK_API_TEST" diff --git a/src/Test/E2E/Helper.gren b/src/Test/E2E/Helper.gren index 68f8819..d40c1bd 100644 --- a/src/Test/E2E/Helper.gren +++ b/src/Test/E2E/Helper.gren @@ -3,7 +3,6 @@ module Test.E2E.Helper exposing , expireFetchToken , expectBadStatus , expectJson - , initDb , get , post , postWithJson @@ -12,10 +11,11 @@ module Test.E2E.Helper exposing import Bytes exposing (Bytes) import Crypto -import Db -import Db.Encode +import Sqlite +import Sqlite.Encode import Email import Expect exposing (Expectation) +import FileSystem import HttpClient exposing (Response) import Json.Decode import Json.Encode @@ -30,12 +30,6 @@ url path = "http://localhost:3000" ++ path -initDb : HttpClient.Permission -> Db.Connection -initDb httpPerm = - -- TODO: get db url from env - Db.init httpPerm "http://localhost:12321/local" - - get : HttpClient.Permission -> String -> Task HttpClient.Error (HttpClient.Response {}) get httpPerm path = url path @@ -87,7 +81,7 @@ expectJson decoder expected bytes = Expect.fail (Json.Decode.errorToString error) -createSession : Db.Connection -> Crypto.SecureContext -> Task Db.Error Session +createSession : Sqlite.Database -> Crypto.SecureContext -> Task Sqlite.Error Session createSession db secureContext = User.findOrCreate db Email.example |> Task.andThen @@ -100,7 +94,7 @@ createSession db secureContext = ) -expireFetchToken : Db.Connection -> Session -> Task Db.Error {} +expireFetchToken : Sqlite.Database -> Session -> Task Sqlite.Error {} expireFetchToken db session = Task.await Time.now <| \now -> let @@ -108,7 +102,7 @@ expireFetchToken db session = (Time.posixToMillis now) - (16 * 60 * 1000) |> Time.millisToPosix in - Db.execute db + Sqlite.execute db { statement = """ update session @@ -116,8 +110,8 @@ expireFetchToken db session = where fetch_session_token = :fetch_session_token """ , parameters = - [ Db.Encode.posix "created" sixteenMinutesAgo - , Db.Encode.string "fetch_session_token" session.fetchSessionToken + [ Sqlite.Encode.time "created" sixteenMinutesAgo + , Sqlite.Encode.string "fetch_session_token" session.fetchSessionToken ] } |> Task.map (\_ -> {}) diff --git a/src/Test/E2E/PublishingIdentity.gren b/src/Test/E2E/PublishingIdentity.gren index 714272e..b5326c6 100644 --- a/src/Test/E2E/PublishingIdentity.gren +++ b/src/Test/E2E/PublishingIdentity.gren @@ -1,7 +1,7 @@ module Test.E2E.PublishingIdentity exposing (tests) -import Db +import Sqlite import Email import Expect import PublishingIdentity @@ -10,19 +10,19 @@ import Test.Runner.Effectful exposing (Test, await, awaitError, concat, describe import User exposing (User) -tests : Db.Connection -> Array Test +tests : Sqlite.Database -> Array Test tests db = let identityName = "duplicate-name-test" - cleanPublishingIdentity : Task Db.Error Int + cleanPublishingIdentity : Task Sqlite.Error (Array Sqlite.ExecutionSummary) cleanPublishingIdentity = - Db.transaction db - [ { statement = "DELETE FROM publishing_identity_users", parameters = [] } - , { statement = "DELETE FROM publishing_identity", parameters = [] } - ] - |> Task.map (\_ -> 0) + [ { statement = "DELETE FROM publishing_identity_users", parameters = [] } + , { statement = "DELETE FROM publishing_identity", parameters = [] } + ] + |> Sqlite.executeAll db + |> Sqlite.inTransaction db in [ describe "PublishingIdentity.create" [ await "clean publishing identity tables" cleanPublishingIdentity <| \_ -> @@ -33,25 +33,27 @@ tests db = [ awaitError "create identity with same name and same user" (PublishingIdentity.create db { user = user1, name = identityName }) <| \error -> test "fails when name already exists for same user" <| \_ -> - expectUniqueConstraintError error + expectUniqueConstraintErrorFor "name" error , awaitError "create identity with same name and different user" (PublishingIdentity.create db { user = user2, name = identityName }) <| \error -> test "fails when name already exists for different user" <| \_ -> - expectUniqueConstraintError error + expectUniqueConstraintErrorFor "name" error ] ] ] -expectUniqueConstraintError : Db.Error -> Expect.Expectation -expectUniqueConstraintError error = +expectUniqueConstraintErrorFor : String -> Sqlite.Error -> Expect.Expectation +expectUniqueConstraintErrorFor field error = when error is - Db.Error message -> - if String.contains "unique" (String.toLower message) then + Sqlite.UniqueConstraintError message -> + if String.contains field (String.toLower message) then Expect.pass else - Expect.fail ("Expected unique constraint error, got: " ++ message) + Expect.fail <| + "Expected unique constraint error for " ++ + field ++ ", got: " ++ message - _ -> - Expect.fail ("Expected Db.Error with unique constraint message, got: " ++ Db.errorToString error) + e -> + Expect.fail ("Expected unique constraint error, got: " ++ Sqlite.errorToString error) diff --git a/src/Test/E2E/Route/PublishingIdentity.gren b/src/Test/E2E/Route/PublishingIdentity.gren index ebe8e14..21fe5d1 100644 --- a/src/Test/E2E/Route/PublishingIdentity.gren +++ b/src/Test/E2E/Route/PublishingIdentity.gren @@ -3,9 +3,9 @@ module Test.E2E.Route.PublishingIdentity exposing (tests) import Crypto import Bytes exposing (Bytes) -import Db -import Db.Encode -import Db.Decode +import Sqlite +import Sqlite.Encode +import Sqlite.Decode import Expect import HttpClient import Json.Decode @@ -14,22 +14,22 @@ import PublishingIdentity exposing (PublishingIdentity) import Session exposing (Session) import Task exposing (Task) import Test.Runner.Effectful exposing (Test, await, awaitError, concat, describe, test, todo) -import Test.E2E.Helper exposing (createSession, initDb, get, post, postWithJson, expectBadStatus, expectJson) +import Test.E2E.Helper exposing (createSession, get, post, postWithJson, expectBadStatus, expectJson) import User exposing (User) -tests : Db.Connection -> HttpClient.Permission -> Crypto.SecureContext -> Array Test +tests : Sqlite.Database -> HttpClient.Permission -> Crypto.SecureContext -> Array Test tests db httpPerm secureContext = let - cleanPublishingIdentity : Task Db.Error Int + cleanPublishingIdentity : Task Sqlite.Error (Array Sqlite.ExecutionSummary) cleanPublishingIdentity = - Db.transaction db - [ { statement = "DELETE FROM publishing_identity_users", parameters = [] } - , { statement = "DELETE FROM publishing_identity", parameters = [] } - ] - |> Task.map (\_ -> 0) + [ { statement = "DELETE FROM publishing_identity_users", parameters = [] } + , { statement = "DELETE FROM publishing_identity", parameters = [] } + ] + |> Sqlite.executeAll db + |> Sqlite.inTransaction db - fetchSessionToken : Session -> Task Db.Error String + fetchSessionToken : Session -> Task Sqlite.Error String fetchSessionToken session = Session.fetch { db = db @@ -66,7 +66,7 @@ tests db httpPerm secureContext = -- GET /publishing-identity (404) - [ awaitError "GET /publishing-identity" + [ awaitError "GET /publishing-identity" (get httpPerm "/publishing-identity") <| \response -> test "404s" <| \_ -> expectBadStatus 404 response @@ -75,7 +75,7 @@ tests db httpPerm secureContext = -- POST /publishing-identity (create) - , awaitError "POST /publishing-identity with no session token" + , awaitError "POST /publishing-identity with no session token" (doPost { sessionToken = Nothing, name = "my-publishing-identity" }) <| \response -> test "Responds with 400 error" <| \_ -> expectBadStatus 400 response diff --git a/src/Test/E2E/Route/Session.gren b/src/Test/E2E/Route/Session.gren index 5ab6043..875af26 100644 --- a/src/Test/E2E/Route/Session.gren +++ b/src/Test/E2E/Route/Session.gren @@ -3,9 +3,9 @@ module Test.E2E.Route.Session exposing (tests) import Bytes import Crypto -import Db -import Db.Encode -import Db.Decode +import Sqlite +import Sqlite.Encode +import Sqlite.Decode import Email import Expect import HttpClient @@ -13,11 +13,11 @@ import Json.Decode import Json.Encode import Task exposing (Task) import Test.Runner.Effectful exposing (Test, await, awaitError, concat, describe, test, todo) -import Test.E2E.Helper exposing (expireFetchToken, createSession, initDb, get, post, postWithJson, expectBadStatus, expectJson) +import Test.E2E.Helper exposing (expireFetchToken, createSession, get, post, postWithJson, expectBadStatus, expectJson) import User exposing (User) -tests : Db.Connection -> HttpClient.Permission -> Crypto.SecureContext -> Array Test +tests : Sqlite.Database -> HttpClient.Permission -> Crypto.SecureContext -> Array Test tests db httpPerm secureContext = @@ -62,7 +62,7 @@ tests db httpPerm secureContext = Expect.equal 200 response.statusCode , await "Getting session info from db" - (Db.getOne db + (Sqlite.getOne db { query = """ select fetch_session_token from session @@ -72,9 +72,11 @@ tests db httpPerm secureContext = limit 1 """ , parameters = - [ Db.Encode.string "email" goodEmail ] - , decoder = - Db.Decode.string "fetch_session_token" + [ Sqlite.Encode.string "email" goodEmail ] + , rowDecoder = + Sqlite.Decode.string + "fetch_session_token" + Sqlite.Decode.succeed } ) (\expectedToken -> @@ -108,7 +110,7 @@ tests db httpPerm secureContext = ] in [ awaitError "with bad token and code" - (doPost { token = "", code = "" }) <| \response -> + (doPost { token = "", code = "" }) <| \response -> test "Responds with 400 error" <| \_ -> expectBadStatus 400 response @@ -129,16 +131,16 @@ tests db httpPerm secureContext = Expect.equal 200 response.statusCode , await "Getting session token from db" - (Db.getOne db + (Sqlite.getOne db { query = """ select token from session where fetch_session_token = :fetch_session_token """ , parameters = - [ Db.Encode.string "fetch_session_token" session.fetchSessionToken ] - , decoder = - Db.Decode.string "token" + [ Sqlite.Encode.string "fetch_session_token" session.fetchSessionToken ] + , rowDecoder = + Sqlite.Decode.string "token" Sqlite.Decode.succeed } ) (\expectedToken -> diff --git a/src/Test/E2E/Session.gren b/src/Test/E2E/Session.gren index 103dc9f..e21329f 100644 --- a/src/Test/E2E/Session.gren +++ b/src/Test/E2E/Session.gren @@ -2,9 +2,9 @@ module Test.E2E.Session exposing (tests) import Crypto -import Db -import Db.Encode -import Db.Decode +import Sqlite +import Sqlite.Encode as Encode +import Sqlite.Decode as Decode import Email import Expect import Fuzz @@ -14,7 +14,7 @@ import Test.Runner.Effectful exposing (Test, await, awaitError, concat, describe import User exposing (User) -tests : Db.Connection -> Crypto.SecureContext -> Array Test +tests : Sqlite.Database -> Crypto.SecureContext -> Array Test tests db secureContext = let sessionSampleSize = @@ -30,10 +30,10 @@ tests db secureContext = } countSessions user = - Db.getOne db + Sqlite.getOne db { query = "select count(*) as count from session where user_id = :user_id" - , parameters = [ Db.Encode.int "user_id" user.id ] - , decoder = Db.Decode.int "count" + , parameters = [ Encode.int "user_id" user.id ] + , rowDecoder = Decode.int "count" Decode.succeed } ambiguousChars = diff --git a/src/Test/E2E/User.gren b/src/Test/E2E/User.gren index 364350b..c074203 100644 --- a/src/Test/E2E/User.gren +++ b/src/Test/E2E/User.gren @@ -1,36 +1,35 @@ module Test.E2E.User exposing (tests) -import Db -import Db.Encode -import Db.Decode +import Sqlite +import Sqlite.Encode as Encode +import Sqlite.Decode as Decode import Email import Expect import Task exposing (Task) import Test.Runner.Effectful exposing (Test, await, concat, describe, test) -import Test.E2E.Helper exposing (initDb) import User exposing (User) -tests : Db.Connection -> Array Test +tests : Sqlite.Database -> Array Test tests db = let - dropUsers : Task Db.Error Int + dropUsers : Task Sqlite.Error Sqlite.ExecutionSummary dropUsers = - Db.execute db + Sqlite.execute db { statement = "delete from user" , parameters = [] } - countUsers : Task Db.Error Int + countUsers : Task Sqlite.Error Int countUsers = - Db.getOne db + Sqlite.getOne db { query = "select count(*) as userCount from user where email = :email" - , parameters = [ Db.Encode.string "email" (Email.toString Email.example) ] - , decoder = Db.Decode.int "userCount" + , parameters = [ Encode.string "email" (Email.toString Email.example) ] + , rowDecoder = Decode.int "userCount" Decode.succeed } - findOrCreateUser : Task Db.Error User + findOrCreateUser : Task Sqlite.Error User findOrCreateUser = User.findOrCreate db Email.example in diff --git a/src/Test/Main.gren b/src/Test/Main.gren index 1504fbc..a76ed70 100644 --- a/src/Test/Main.gren +++ b/src/Test/Main.gren @@ -1,13 +1,14 @@ module Test.Main exposing (main) import Expect +import FileSystem import HttpClient import Init import Node -import Registry.Db +import Registry.Db as Db +import Task import Test import Test.E2E -import Test.E2E.Helper as Helper import Test.Runner.Effectful as Effectful exposing (await) import Test.Unit @@ -15,13 +16,15 @@ main : Effectful.Program a main = Node.defineSimpleProgram <| \env -> Init.await HttpClient.initialize <| \httpPerm -> + Init.await FileSystem.initialize <| \fsPerm -> Effectful.run env <| - let - migrate = - Registry.Db.migrate (Helper.initDb httpPerm) - in Effectful.concat [ Effectful.wrap <| Test.describe "Unit tests" Test.Unit.tests - , await "Run migrations for E2E tests" migrate <| \_ -> - Effectful.describe "E2E tests" (Test.E2E.tests httpPerm) + , await "Init DB for E2E tests" (Db.open fsPerm) <| \db -> + await "Run migrations for E2E tests" (Db.migrate db) <| \_ -> + Effectful.describe "E2E tests" <| + Test.E2E.tests + { httpPerm = httpPerm + , db = db + } ] diff --git a/src/User.gren b/src/User.gren index d411297..41c5aa3 100644 --- a/src/User.gren +++ b/src/User.gren @@ -6,9 +6,9 @@ module User exposing ) -import Db -import Db.Decode as Decode exposing (Decoder) -import Db.Encode as Encode +import Sqlite +import Sqlite.Decode as Decode exposing (Decoder) +import Sqlite.Encode as Encode import Email exposing (Email) import Task exposing (Task) import Time @@ -23,14 +23,14 @@ type alias User = decoder : Decoder User decoder = - Decode.field Decode.string "email" <| \email -> + Decode.string "email" <| \email -> when (Email.fromString email) is Nothing -> Decode.fail ("Invalid email: " ++ email) Just e -> - Decode.field Decode.int "id" <| \id -> - Decode.field Decode.posix "created" <| \created -> + Decode.int "id" <| \id -> + Decode.time "created" <| \created -> Decode.succeed { id = id , created = created @@ -38,9 +38,9 @@ decoder = } -findBySessionToken : Db.Connection -> String -> Task Db.Error User +findBySessionToken : Sqlite.Database -> String -> Task Sqlite.Error User findBySessionToken db sessionToken = - Db.getOne db + Sqlite.getOne db { query = """ SELECT user.id, user.email, user.created @@ -49,11 +49,11 @@ findBySessionToken db sessionToken = WHERE session.token = :token """ , parameters = [ Encode.string "token" sessionToken ] - , decoder = decoder + , rowDecoder = decoder } -findOrCreate : Db.Connection -> Email -> Task Db.Error User +findOrCreate : Sqlite.Database -> Email -> Task Sqlite.Error User findOrCreate db email = let encodedEmail = @@ -61,19 +61,19 @@ findOrCreate db email = createUser = Task.await Time.now <| \now -> - Db.execute db + Sqlite.execute db { statement = "INSERT OR IGNORE INTO user (created, email) VALUES (:created, :email)" , parameters = - [ Encode.posix "created" now + [ Encode.time "created" now , encodedEmail ] } getUser = - Db.getOne db + Sqlite.getOne db { query = "SELECT * FROM USER WHERE email = :email" , parameters = [ encodedEmail ] - , decoder = decoder + , rowDecoder = decoder } in createUser |> Task.andThen (\_ -> getUser)