diff --git a/.envrc b/.envrc index cffc922b0..a7d0f9482 100644 --- a/.envrc +++ b/.envrc @@ -1 +1,2 @@ use flake . --impure +export IHP_LIB=lib/IHP/ \ No newline at end of file diff --git a/.gitignore b/.gitignore index ba0123cec..e8d960f41 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,5 @@ devenv.local.nix # nix build output links result* + +.idea diff --git a/Guide/database.markdown b/Guide/database.markdown index 954c42328..ed1c145c6 100644 --- a/Guide/database.markdown +++ b/Guide/database.markdown @@ -765,18 +765,10 @@ Similarly as for renaming, deleting a column currently won't work automatically 1. Delete your column in the Schema Designer 2. Delete the column from the database by executing `ALTER TABLE tablename DROP COLUMN colname` -### Alternate Method - -There's always more than one way. This is another. - -1. Make changes in the Schema Designer -2. Click `Save DB to Fixtures` in the Schema Designer (Use the arrow next to the `Update DB` button to see this option) -3. Edit `Fixtures.sql` to your heart's content. -4. Click `Push to DB` in the Schema Designer (Use the arrow next to the `Update DB` button to see this option) ### Migrations In Production -IHP currently has no built-in migration system yet. We're still experimenting with a great way to solve this. Until then, the recommended approach used by digitally induced is to manually migrate your database using DDL statements as shown above. +See the [Migrations guide](https://ihp.digitallyinduced.com/Guide/database-migrations.html). ## Supported Database Types @@ -848,3 +840,16 @@ action CreateUserAction = do redirectTo NewSessionAction ``` + +## Unique Constraints + +It's possible to use the UI to set the unique constraint on a column. However, sometimes you might want to add a unique constraint on multiple columns. This can be done by adding a unique constraint to the `Schema.sql` file. For example, to add a unique constraint on the `email` and `username` columns of the `users` table, you would add the following to the `Schema.sql` file: + +```sql +CREATE TABLE users ( + id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, + email TEXT NOT NULL, + username TEXT NOT NULL, + UNIQUE (email, username) +); +``` diff --git a/Guide/deployment.markdown b/Guide/deployment.markdown index 643e875b1..6bc69feb2 100644 --- a/Guide/deployment.markdown +++ b/Guide/deployment.markdown @@ -10,19 +10,151 @@ IHP comes with a standard command called `deploy-to-nixos`. This tool is a littl AWS EC2 is a good choice for deploying IHP in a professional setup. -### Creating a new EC2 Instance +### AWS infrastructure preparation + +#### Creating a new EC2 Instance Start a new EC2 instance and use the official NixOS AMI `NixOS-23.05.426.afc48694f2a-x86_64-linux`. You can find the latest NixOS AMI at https://nixos.org/download#nixos-amazon Example steps: - Visit [EC2 creation page](https://eu-west-1.console.aws.amazon.com/ec2/home?region=eu-west-1#LaunchInstances:) in your desired region. - - Select AMI by name, it will appear under "Community AMIs" after searching by name. - - Select at least a `t3a.small` instance size to have enough RAM for the compilation - - Specify a generous root disk volume. By nature NixOS can consume lots of disk space as you trial-and-error your application deployment. As a minimum, we advise 60 GiB + - Select AMI by name, it will appear under "Community AMIs" after searching by name (there can be a slight delay before the result appears as it searches in all community AMIs). + - Select at least a `t3a.small` instance size to have enough RAM for the compilation. For a real-world application, chances are that you need `t3a.medium` to successfully compile it. + - Specify a generous root disk volume. By nature NixOS can consume lots of disk space as you trial-and-error your application deployment. As a minimum, we advise 60 GiB. - Under `Network settings`, allow SSH traffic from your IP address only, allow HTTPS and HTTP traffic from the internet. Due to the certificate validation for Let's Encrypt, even if your application does not need to have it, allow HTTP too. - Make sure to attach SSH keys to the instance at creation time, that is available locally, so you can SSH to the EC2 instance without password later. + - Either before creating the EC2 instance, you import your existing keypair to [EC2 Key Pairs](https://us-east-1.console.aws.amazon.com/ec2/home?region=eu-west-1#ImportKeyPair:), then you should select it at the EC2 creation page. + - Or let AWS create one on-the-fly: ![image](https://github.com/digitallyinduced/ihp/assets/114076/317b022a-ad6e-43ae-931d-8710db0b711c) . Afterwards, you will be able to download the private key file, later on it is referred as `ihp-app.pem` in this documentation. + +#### (Optional) Creating an RDS Instance + +For production systems, it is advised to use a fully managed PostgreSQL instance, it can be multi-region, fault tolerant, but most of all, +daily backups happen automatically with configurable retention. + +To switch from the local PostgreSQL instance to a managed one (you can do it after or before the initial deployment), you can execute the following steps: + - Visit [RDS creation page](https://us-east-1.console.aws.amazon.com/rds/home?region=eu-west-1#launch-dbinstance:) in your desired region. + - Select PostgreSQL as the Engine Type. + - Select a compatible Engine Version, there are good chances that the very last version will fit. + - At Templates, Choose `Free Tier` for any non-live environments, `Production` for the live environment. + - (Optional) Choose `Auto generate password` for having a secure master password. + - Choose `Connect to an EC2 compute resource` and select your already existing EC2 instance. + - Then you can `Create database`. This process is slow, check back in 10 minutes or so afterward. Note down the auto-generated password. + - Edit your `flake.nix`, under `flake.nixosConfigurations."ihp-app".services.ihp`, you can specify the database URL like: `databaseUrl = lib.mkForce "postgresql://postgres:YOUR-PASSWORD@YOUR-HOSTNAME.amatonaws.com/postgres";`. You can find the proper hostname after the initialization is complete, on the RDS instance detail page. + - `pg_dump --no-owner --no-acl` your existing local database on the EC2 instance directly, and then, you can load it to the newly created instance via `pgsql`. `deploy-to-nixos` won't populate the initial schema at an existing remote database, that's why dumping, `scp d`ing and loading it via `psql` is necessary. + +#### (Optional) Creating an S3 bucket + +If your application needs to store files, on AWS, those should use an S3 bucket for that. + +Infrastructure-side preparation: + - Visit the [S3 creation page](https://s3.console.aws.amazon.com/s3/bucket/create?region=eu-west-1) and create a bucket in the same region..If objects should or should not be public, it's up to the application's business requirements. The S3 [ARN](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference-arns.html) from the S3 details page should be noted down. + - Create an new IAM user for the S3 access. Create an [AWS access key](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html) for that IAM user. + - For that user, attach a policy that allows access to the bucket, for example: +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "VisualEditor1", + "Effect": "Allow", + "Action": "s3:*", + "Resource": [ + "YOUR-BUCKET-ARN", + "YOUR-BUCKET-ARN/*" + ] + } + ] +} +``` + - See the [Storage guide](https://ihp.digitallyinduced.com/Guide/file-storage.html#s3) on how to use the access key. + + If your application requires so, make the S3 bucket publicly available. + - Go to https://s3.console.aws.amazon.com/s3/buckets/YOUR-BUCKET?region=eu-west-1&bucketType=general&tab=permissions (permissions tab of the S3 bucket) + - Set `Block all public access` to entirely off. + - Set a bucket policy like this: +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "PublicReadGetObject", + "Effect": "Allow", + "Principal": "*", + "Action": "s3:GetObject", + "Resource": "YOUR-BUCKET-ARN/*" + } + ] +} +``` + - Test the access by locating a file in a bucket under Objects and "Copy S3 URI" for it. + +#### (Optional) Connecting CloudWatch + +For a production system, logging is essential, so you are informed about anomalies before customer complaints, or you are able to provide an evidence for an incident and so on. + +Mind the region of your EC2 instance for these steps. + +- [Create a CloudWatch log group](https://eu-west-1.console.aws.amazon.com/cloudwatch/home?region=eu-west-1#logsV2:log-groups/create-log-group), note down the ARN. +- Create a log stream inside the previously created log group, for instance `in`. +- Create an IAM user with an access key and secret with the following policy: +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "logs:CreateLogStream", + "logs:PutLogEvents", + "logs:DescribeLogStreams" + ], + "Resource": [ + "[YOUR-GROUP-ARN]", + "[YOUR-GROUP-ARN]:*" + ] + } + ] +} +``` +- Configure the `services.vector` part in your `flake.nix` to activate logging: +``` +services.vector = { + enable = true; + journaldAccess = true; + settings = { + sources.journald = { + type = "journald"; + include_units = ["app.service" "nginx.service" "worker.service"]; + }; + transforms.remap_remove_specific_keys = { + type = "remap"; + inputs = ["journald"]; + source = '' + del(._STREAM_ID) + del(._SYSTEMD_UNIT) + del(._BOOT_ID) + del(.source_type) + ''; + }; + sinks.out = { + auth = { + access_key_id = "YOUR-IAM-ACCESS-KEY"; + secret_access_key = "YOUR-IAM-ACCESS-KEY"; + }; + inputs = ["remap_remove_specific_keys"]; + type = "aws_cloudwatch_logs"; + compression = "gzip"; + encoding.codec = "json"; + region = "us-east-1"; + group_name = "tpp-qa"; + stream_name = "in"; + }; + }; +}; +``` +- Review the incoming log entries, adjust remapping accordingly. You might want to remove or transform more entries to make the logs useful for alerts or accountability. -### Connecting to the EC2 Instance +### Connecting to the EC2 / Virtual Machine Instance After you've created the instance, configure your local SSH settings to point to the instance. diff --git a/Guide/editors.markdown b/Guide/editors.markdown index 26f78b3c4..76de50f3e 100644 --- a/Guide/editors.markdown +++ b/Guide/editors.markdown @@ -149,6 +149,70 @@ and `chmod +x ~/bin/emacs-line`, then export this env var in your shell (e.g. in export IHP_EDITOR="$HOME/bin/emacs-line" ``` +Another useful package set that integrates with lsp/lsp-ui and loads the default nix environment from direnv as well as removing [common flycheck issue](https://github.com/joncol/dotfiles/blob/master/homedir/.emacs.d/init.el). +This config also adds a jump to definition for functions bound to "C-c p": + +```emacs +(use-package direnv + :defer + :custom + (direnv-always-show-summary nil) + :config + (direnv-mode)) +(use-package lsp-mode + :custom + (lsp-lens-enable nil) + (lsp-enable-symbol-highlighting nil) + + :hook + (lsp-mode . lsp-enable-which-key-integration) + + :config + ;; This is to make `lsp-mode' work with `direnv' and pick up the correct + ;; version of GHC. + (advice-add 'lsp :before #'direnv-update-environment) + (setq lsp-modeline-code-actions-enable nil)) + +(use-package lsp-ui + :hook (prog-mode . lsp-ui-mode) + :bind (("C-c p" . lsp-ui-peek-find-definitions)) + :config + (setq lsp-ui-doc-position 'bottom)) + +;; (add-hook 'haskell-mode-hook #'lsp) +(use-package flycheck-haskell + ;; Disabling this package, since it only gives error: + ;; "Reading Haskell configuration failed with exit code Segmentation fault and + ;; output:", when trying to run it in Nix/direnv setup. + :disabled + :hook (haskell-mode . flycheck-haskell-setup)) + +(add-hook 'haskell-mode-hook + (lambda () + (rainbow-mode -1) +;; we aren't evil: +;; (evil-leader/set-key "x h" 'haskell-hoogle) +;; (setq evil-shift-width 2) + (define-key haskell-mode-map (kbd "C-c C-c C-s") + 'haskell-mode-stylish-buffer) + (bind-key (kbd "C-c C-c C-a") 'haskell-sort-imports) + (setq haskell-auto-insert-module-format-string + "module %s\n () where\n\n") + (haskell-auto-insert-module-template) + (smartparens-mode) + (sp-local-pair 'haskell-mode "{" "}") + (setq haskell-hoogle-command nil) + (ligature-mode))) + +(use-package lsp-haskell + :hook ((haskell-mode . lsp-deferred) + (haskell-literate-mode . lsp-deferred)) + :custom + (lsp-haskell-server-path "haskell-language-server")) + +(use-package haskell-mode + :defer) +``` ## Using IHP with Vim / NeoVim diff --git a/Guide/search/package-lock.json b/Guide/search/package-lock.json index c41bd2464..4e84da9b5 100644 --- a/Guide/search/package-lock.json +++ b/Guide/search/package-lock.json @@ -177,6 +177,29 @@ "react-dom": ">= 16.8.0 < 18.0.0" } }, + "node_modules/@types/prop-types": { + "version": "15.7.11", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", + "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==", + "peer": true + }, + "node_modules/@types/react": { + "version": "17.0.71", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.71.tgz", + "integrity": "sha512-lfqOu9mp16nmaGRrS8deS2Taqhd5Ih0o92Te5Ws6I1py4ytHBcXLqh0YIqVsViqwVI5f+haiFM6hju814BzcmA==", + "peer": true, + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/scheduler": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", + "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==", + "peer": true + }, "node_modules/algoliasearch": { "version": "4.10.5", "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-4.10.5.tgz", @@ -198,6 +221,12 @@ "@algolia/transporter": "4.10.5" } }, + "node_modules/csstype": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==", + "peer": true + }, "node_modules/esbuild": { "version": "0.13.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.13.2.tgz", @@ -629,6 +658,29 @@ "algoliasearch": "^4.0.0" } }, + "@types/prop-types": { + "version": "15.7.11", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", + "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==", + "peer": true + }, + "@types/react": { + "version": "17.0.71", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.71.tgz", + "integrity": "sha512-lfqOu9mp16nmaGRrS8deS2Taqhd5Ih0o92Te5Ws6I1py4ytHBcXLqh0YIqVsViqwVI5f+haiFM6hju814BzcmA==", + "peer": true, + "requires": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "@types/scheduler": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", + "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==", + "peer": true + }, "algoliasearch": { "version": "4.10.5", "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-4.10.5.tgz", @@ -650,6 +702,12 @@ "@algolia/transporter": "4.10.5" } }, + "csstype": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==", + "peer": true + }, "esbuild": { "version": "0.13.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.13.2.tgz", diff --git a/Guide/view.markdown b/Guide/view.markdown index 6a6f4419d..4e326c972 100644 --- a/Guide/view.markdown +++ b/Guide/view.markdown @@ -181,6 +181,58 @@ Now the `company` variable can be used to read the current user's company across Use [`theRequest`](https://ihp.digitallyinduced.com/api-docs/IHP-ViewSupport.html#v:theRequest) to access the current [WAI request](https://hackage.haskell.org/package/wai-3.2.2.1/docs/Network-Wai.html). +### Passing JSON to the View + +You might need to pass JSON values to the view, so later you could have a JS script to read it. You should use Aeson's `toJSON` function to convert your data to JSON and then pass it to the view. Let's say you have a `Posts` controller and `Post`s records that you want to print in the console. + +```haskell +-- Web/Controller/Posts.hs +instance Controller PostsController where + action PostsAction = do + posts <- query @Post |> fetch + + render IndexView { .. } + +``` + +Then in the view, you can access the JSON data like this: + +```haskell +-- Web/View/Posts/Index.hs + +module Web.View.Posts.Index where +import Web.View.Prelude + +-- Add Aeson import. +import Data.Aeson (encode) + +data IndexView = IndexView { posts :: [Post] } + +instance View IndexView where + html IndexView { .. } = [hsx| +
+ Open the developer's console to see the posts JSON data. +
+ {- Pass the encoded JSON to the JS script -} + + |] + where + postsToJson :: [Post] -> Value + postsToJson posts = + posts + |> fmap (\post -> object + [ "id" .= post.id + , "title" .= post.title + , "body" .= post.body + ]) + |> toJSON +``` + +No you can go to `/Posts`, create a few posts, and see their JSON in the browser's developer console. + ### Highlighting the current active link Use [`isActiveAction`](https://ihp.digitallyinduced.com/api-docs/IHP-ViewSupport.html#v:isActiveAction) to check whether the current request URL matches a given action: diff --git a/IHP/ApplicationContext.hs b/IHP/ApplicationContext.hs index a21fcd8dc..97c16dc04 100644 --- a/IHP/ApplicationContext.hs +++ b/IHP/ApplicationContext.hs @@ -1,15 +1,12 @@ module IHP.ApplicationContext where import IHP.Prelude -import Network.Wai.Session (Session) -import qualified Data.Vault.Lazy as Vault import IHP.AutoRefresh.Types (AutoRefreshServer) import IHP.FrameworkConfig (FrameworkConfig) import IHP.PGListener (PGListener) data ApplicationContext = ApplicationContext { modelContext :: !ModelContext - , session :: !(Vault.Key (Session IO ByteString ByteString)) , autoRefreshServer :: !(IORef AutoRefreshServer) , frameworkConfig :: !FrameworkConfig , pgListener :: PGListener diff --git a/IHP/Controller/Context.hs b/IHP/Controller/Context.hs index 61a8cd3bc..0ddb744cd 100644 --- a/IHP/Controller/Context.hs +++ b/IHP/Controller/Context.hs @@ -10,6 +10,7 @@ import qualified Data.Typeable as Typeable import IHP.Controller.RequestContext import IHP.FrameworkConfig import IHP.Log.Types +import System.IO.Unsafe (unsafePerformIO) -- | A container storing useful data along the request lifecycle, such as the request, the current user, set current view layout, flash messages, ... -- @@ -137,5 +138,28 @@ instance HasField "frameworkConfig" ControllerContext FrameworkConfig where getField controllerContext = controllerContext.requestContext.frameworkConfig {-# INLINABLE getField #-} +-- The following hack is bad, but allows us to override the logger using 'putContext' +-- The alternative would be https://github.com/digitallyinduced/ihp/pull/1921 which is also not very nice +-- +-- This can be useful to customize the log formatter for all actions of an app: +-- +-- > import IHP.Log.Types +-- > import IHP.Controller.Context +-- > +-- > instance InitControllerContext WebApplication where +-- > initContext = do +-- > let defaultLogger :: Logger = ?context.frameworkConfig.logger +-- > let withUserIdLogger :: Logger = { Log.formatter = userIdFormatter defaultLogger.formatter } +-- > putContext withUserIdLogger +-- > +-- > userIdFormatter :: (?context :: Context) => Log.LogFormatter -> Log.LogFormatter +-- > userIdFormatter existingFormatter time level string = +-- > existingFormatter time level (prependUserId string) +-- > +-- > preprendUserId :: (?context :: Context) => Text -> Text +-- > preprendUserId string = "userId: " <> show currentUserId <> " " <> string +-- +-- This design mistake should be fixed in IHP v2 instance HasField "logger" ControllerContext Logger where - getField controllerContext = controllerContext.frameworkConfig.logger + getField context@(FrozenControllerContext { customFields }) = fromMaybe context.frameworkConfig.logger (TypeMap.lookup @Logger customFields) + getField context = (unsafePerformIO (freeze context)).logger -- Hacky, but there's no better way. The only way to retrieve the logger here, is by reading from the IORef in an unsafe way diff --git a/IHP/Controller/RequestContext.hs b/IHP/Controller/RequestContext.hs index bf6a46ddf..5bac8c508 100644 --- a/IHP/Controller/RequestContext.hs +++ b/IHP/Controller/RequestContext.hs @@ -8,8 +8,6 @@ import ClassyPrelude import qualified Data.ByteString.Lazy as LBS import Network.Wai (Request, Response, ResponseReceived) import Network.Wai.Parse (File, Param) -import qualified Data.Vault.Lazy as Vault -import Network.Wai.Session (Session) import IHP.FrameworkConfig import qualified Data.Aeson as Aeson @@ -24,6 +22,5 @@ data RequestContext = RequestContext { request :: Request , respond :: Respond , requestBody :: RequestBody - , vault :: (Vault.Key (Session IO ByteString ByteString)) , frameworkConfig :: FrameworkConfig } diff --git a/IHP/Controller/Response.hs b/IHP/Controller/Response.hs index 5a79af936..4dddf3f70 100644 --- a/IHP/Controller/Response.hs +++ b/IHP/Controller/Response.hs @@ -9,12 +9,11 @@ where import ClassyPrelude import Network.HTTP.Types.Header import qualified IHP.Controller.Context as Context -import IHP.Controller.Context (ControllerContext(ControllerContext)) import qualified Network.Wai import Network.Wai (Response) import qualified Control.Exception as Exception -respondAndExit :: (?context::ControllerContext) => Response -> IO () +respondAndExit :: (?context :: Context.ControllerContext) => Response -> IO () respondAndExit response = do responseWithHeaders <- addResponseHeadersFromContext response Exception.throwIO (ResponseException responseWithHeaders) @@ -35,7 +34,7 @@ addResponseHeaders headers = Network.Wai.mapResponseHeaders (\hs -> headers <> h -- > addResponseHeadersFromContext response -- You probabaly want `setHeader` -- -addResponseHeadersFromContext :: (?context :: ControllerContext) => Response -> IO Response +addResponseHeadersFromContext :: (?context :: Context.ControllerContext) => Response -> IO Response addResponseHeadersFromContext response = do maybeHeaders <- Context.maybeFromContext @[Header] let headers = fromMaybe [] maybeHeaders diff --git a/IHP/Controller/Session.hs b/IHP/Controller/Session.hs index d0e633c1c..3d5c207e6 100644 --- a/IHP/Controller/Session.hs +++ b/IHP/Controller/Session.hs @@ -24,6 +24,7 @@ module IHP.Controller.Session , getSessionEither , deleteSession , getSessionAndClear + , sessionVaultKey ) where import IHP.Prelude @@ -36,6 +37,8 @@ import qualified Network.Wai as Wai import qualified Data.Serialize as Serialize import Data.Serialize (Serialize) import Data.Serialize.Text () +import qualified Network.Wai.Session +import System.IO.Unsafe (unsafePerformIO) -- | Types of possible errors as a result of -- requesting a value from the session storage @@ -161,5 +164,9 @@ sessionVault = case vaultLookup of Just session -> session Nothing -> error "sessionInsert: The session vault is missing in the request" where - RequestContext { request, vault } = ?context.requestContext - vaultLookup = Vault.lookup vault (Wai.vault request) + RequestContext { request } = ?context.requestContext + vaultLookup = Vault.lookup sessionVaultKey request.vault + +sessionVaultKey :: Vault.Key (Network.Wai.Session.Session IO ByteString ByteString) +sessionVaultKey = unsafePerformIO Vault.newKey +{-# NOINLINE sessionVaultKey #-} \ No newline at end of file diff --git a/IHP/ControllerSupport.hs b/IHP/ControllerSupport.hs index a5468b0ef..900d66695 100644 --- a/IHP/ControllerSupport.hs +++ b/IHP/ControllerSupport.hs @@ -29,7 +29,7 @@ module IHP.ControllerSupport import ClassyPrelude import IHP.HaskellSupport -import Network.Wai (Response, Request, ResponseReceived, responseLBS, requestHeaders) +import Network.Wai (Request, ResponseReceived, responseLBS, requestHeaders) import qualified Network.HTTP.Types as HTTP import qualified Network.Wai import IHP.ModelSupport @@ -39,7 +39,6 @@ import qualified Data.ByteString.Lazy import qualified IHP.Controller.RequestContext as RequestContext import IHP.Controller.RequestContext (RequestContext, Respond) import qualified Data.CaseInsensitive -import qualified Control.Exception as Exception import qualified IHP.ErrorController as ErrorController import qualified Data.Typeable as Typeable import IHP.FrameworkConfig (FrameworkConfig (..), ConfigProvider(..)) @@ -66,6 +65,9 @@ class InitControllerContext application where initContext = pure () {-# INLINABLE initContext #-} +instance InitControllerContext () where + initContext = pure () + {-# INLINE runAction #-} runAction :: forall controller. (Controller controller, ?context :: ControllerContext, ?modelContext :: ModelContext, ?applicationContext :: ApplicationContext, ?requestContext :: RequestContext) => controller -> IO ResponseReceived runAction controller = do @@ -149,12 +151,11 @@ prepareRLSIfNeeded modelContext = do Nothing -> pure modelContext {-# INLINE startWebSocketApp #-} -startWebSocketApp :: forall webSocketApp application. (?applicationContext :: ApplicationContext, ?context :: RequestContext, InitControllerContext application, ?application :: application, Typeable application, WebSockets.WSApp webSocketApp) => IO ResponseReceived -> IO ResponseReceived -startWebSocketApp onHTTP = do +startWebSocketApp :: forall webSocketApp application. (?applicationContext :: ApplicationContext, ?context :: RequestContext, InitControllerContext application, ?application :: application, Typeable application, WebSockets.WSApp webSocketApp) => IO ResponseReceived -> Network.Wai.Application +startWebSocketApp onHTTP request respond = do let ?modelContext = ?applicationContext.modelContext - let ?requestContext = ?context - let respond = ?context.respond - let request = ?context.request + requestContext <- createRequestContext ?applicationContext request respond + let ?requestContext = requestContext let handleConnection pendingConnection = do connection <- WebSockets.acceptRequest pendingConnection @@ -177,7 +178,7 @@ startWebSocketApp onHTTP = do Just response -> respond response Nothing -> onHTTP {-# INLINE startWebSocketAppAndFailOnHTTP #-} -startWebSocketAppAndFailOnHTTP :: forall webSocketApp application. (?applicationContext :: ApplicationContext, ?context :: RequestContext, InitControllerContext application, ?application :: application, Typeable application, WebSockets.WSApp webSocketApp) => IO ResponseReceived +startWebSocketAppAndFailOnHTTP :: forall webSocketApp application. (?applicationContext :: ApplicationContext, ?context :: RequestContext, InitControllerContext application, ?application :: application, Typeable application, WebSockets.WSApp webSocketApp) => Network.Wai.Application startWebSocketAppAndFailOnHTTP = startWebSocketApp @webSocketApp @application (respond $ responseLBS HTTP.status400 [(hContentType, "text/plain")] "This endpoint is only available via a WebSocket") where respond = ?context.respond @@ -257,7 +258,7 @@ requestBodyJSON = {-# INLINE createRequestContext #-} createRequestContext :: ApplicationContext -> Request -> Respond -> IO RequestContext -createRequestContext ApplicationContext { session, frameworkConfig } request respond = do +createRequestContext ApplicationContext { frameworkConfig } request respond = do let contentType = lookup hContentType (requestHeaders request) requestBody <- case contentType of "application/json" -> do @@ -268,7 +269,7 @@ createRequestContext ApplicationContext { session, frameworkConfig } request res (params, files) <- WaiParse.parseRequestBodyEx frameworkConfig.parseRequestBodyOptions WaiParse.lbsBackEnd request pure RequestContext.FormBody { .. } - pure RequestContext.RequestContext { request, respond, requestBody, vault = session, frameworkConfig } + pure RequestContext.RequestContext { request, respond, requestBody, frameworkConfig } -- | Returns a custom config parameter diff --git a/IHP/EnvVar.hs b/IHP/EnvVar.hs index a2e164bb8..0ae52926f 100644 --- a/IHP/EnvVar.hs +++ b/IHP/EnvVar.hs @@ -11,7 +11,6 @@ import IHP.Prelude import Data.String.Interpolate.IsString (i) import qualified System.Posix.Env.ByteString as Posix import Network.Socket (PortNumber) -import Data.Word (Word16) import IHP.Mail.Types import IHP.Environment diff --git a/IHP/ErrorController.hs b/IHP/ErrorController.hs index 29b84d9b8..3ff0f5a80 100644 --- a/IHP/ErrorController.hs +++ b/IHP/ErrorController.hs @@ -78,11 +78,12 @@ displayException exception action additionalInfo = do -- to the error tracking service (e.g. sentry). Usually this service also writes -- the error message to the stderr output -- - let exceptionTracker = ?applicationContext.frameworkConfig.exceptionTracker.onException - let request = ?requestContext.request + when (?context.frameworkConfig.environment == Environment.Production) do + let exceptionTracker = ?applicationContext.frameworkConfig.exceptionTracker.onException + let request = ?requestContext.request - exceptionTracker (Just request) exception + exceptionTracker (Just request) exception supportingHandlers |> head @@ -316,9 +317,10 @@ recordNotFoundExceptionHandlerProd exception controller additionalInfo = in Just (handleNotFound ?context.request ?context.respond) Nothing -> Nothing -handleRouterException :: (?context :: RequestContext) => SomeException -> IO ResponseReceived -handleRouterException exception = - case fromException exception of +handleRouterException :: (?applicationContext :: ApplicationContext) => SomeException -> Application +handleRouterException exception request respond = + let ?context = ?applicationContext + in case fromException exception of Just Router.NoConstructorMatched { expectedType, value, field } -> do let errorMessage = [hsx|

Routing failed with: {tshow exception}

@@ -329,14 +331,12 @@ handleRouterException exception = let title = case value of Just value -> [hsx|Expected {expectedType} for field {field} but got {value}|] Nothing -> [hsx|The action was called without the required {field} parameter|] - let RequestContext { respond } = ?context respond $ responseBuilder status400 [(hContentType, "text/html")] (Blaze.renderHtmlBuilder (renderError title errorMessage)) Just Router.BadType { expectedType, value = Just value, field } -> do let errorMessage = [hsx|

Routing failed with: {tshow exception}

|] let title = [hsx|Query parameter {field} needs to be a {expectedType} but got {value}|] - let RequestContext { respond } = ?context respond $ responseBuilder status400 [(hContentType, "text/html")] (Blaze.renderHtmlBuilder (renderError title errorMessage)) _ -> case fromException exception of Just Router.UnexpectedMethodException { allowedMethods = [Router.DELETE], method = Router.GET } -> do @@ -371,7 +371,6 @@ handleRouterException exception =

|] let title = [hsx|Action was called from a GET request, but needs to be called as a DELETE request|] - let RequestContext { respond } = ?context respond $ responseBuilder status400 [(hContentType, "text/html")] (Blaze.renderHtmlBuilder (renderError title errorMessage)) Just Router.UnexpectedMethodException { allowedMethods = [Router.POST], method = Router.GET } -> do let errorMessage = [hsx| @@ -386,7 +385,6 @@ handleRouterException exception =

|] let title = [hsx|Action was called from a GET request, but needs to be called as a POST request|] - let RequestContext { respond } = ?context respond $ responseBuilder status400 [(hContentType, "text/html")] (Blaze.renderHtmlBuilder (renderError title errorMessage)) Just Router.UnexpectedMethodException { allowedMethods, method } -> do let errorMessage = [hsx| @@ -397,7 +395,6 @@ handleRouterException exception =

|] let title = [hsx|Action was called with a {method} request, but needs to be called with one of these request methods: {allowedMethods}|] - let RequestContext { respond } = ?context respond $ responseBuilder status400 [(hContentType, "text/html")] (Blaze.renderHtmlBuilder (renderError title errorMessage)) _ -> do let errorMessage = [hsx| @@ -407,7 +404,6 @@ handleRouterException exception =

Are you trying to do a DELETE action, but your link is missing class="js-delete"?

|] let title = H.text "Routing failed" - let RequestContext { respond } = ?context respond $ responseBuilder status500 [(hContentType, "text/html")] (Blaze.renderHtmlBuilder (renderError title errorMessage)) diff --git a/IHP/IDE/FileWatcher.hs b/IHP/IDE/FileWatcher.hs index 7713e50d1..d380eb9d2 100644 --- a/IHP/IDE/FileWatcher.hs +++ b/IHP/IDE/FileWatcher.hs @@ -9,7 +9,6 @@ import System.Directory (listDirectory, doesDirectoryExist) import qualified Data.Map as Map import qualified System.FSNotify as FS import IHP.IDE.Types -import qualified Data.Time.Clock as Clock import qualified Data.List as List import IHP.IDE.LiveReloadNotificationServer (notifyAssetChange) import qualified Control.Debounce as Debounce diff --git a/IHP/IDE/ToolServer.hs b/IHP/IDE/ToolServer.hs index 722d58478..2f7d09747 100644 --- a/IHP/IDE/ToolServer.hs +++ b/IHP/IDE/ToolServer.hs @@ -6,13 +6,11 @@ import qualified Network.Wai.Handler.Warp as Warp import IHP.IDE.Types import IHP.IDE.PortConfig import qualified IHP.ControllerSupport as ControllerSupport -import qualified IHP.ErrorController as ErrorController import IHP.ApplicationContext import IHP.ModelSupport import IHP.RouterSupport hiding (get) import Network.Wai.Session.ClientSession (clientsessionStore) import qualified Web.ClientSession as ClientSession -import qualified Data.Vault.Lazy as Vault import Network.Wai.Middleware.MethodOverridePost (methodOverridePost) import Network.Wai.Session (withSession) import qualified Network.WebSockets as Websocket @@ -37,6 +35,7 @@ import qualified System.Process as Process import System.Info import qualified IHP.EnvVar as EnvVar import qualified IHP.AutoRefresh.Types as AutoRefresh +import qualified IHP.AutoRefresh as AutoRefresh import IHP.Controller.Context import qualified IHP.IDE.ToolServer.Layout as Layout import IHP.Controller.Layout @@ -48,6 +47,7 @@ import qualified IHP.PGListener as PGListener import qualified Network.Wai.Application.Static as Static import qualified WaiAppStatic.Types as Static import IHP.Controller.NotFound (handleNotFound) +import IHP.Controller.Session (sessionVaultKey) withToolServer :: (?context :: Context) => IO () -> IO () withToolServer inner = withAsyncBound async (\_ -> inner) @@ -71,21 +71,18 @@ startToolServer' port isDebugMode = do Just baseUrl -> Config.option $ Config.BaseUrl baseUrl Nothing -> pure () - session <- Vault.newKey store <- fmap clientsessionStore (ClientSession.getKey "Config/client_session_key.aes") - let sessionMiddleware :: Wai.Middleware = withSession store "SESSION" (frameworkConfig.sessionCookie) session + let sessionMiddleware :: Wai.Middleware = withSession store "SESSION" (frameworkConfig.sessionCookie) sessionVaultKey let modelContext = notConnectedModelContext undefined pgListener <- PGListener.init modelContext autoRefreshServer <- newIORef (AutoRefresh.newAutoRefreshServer pgListener) staticApp <- initStaticApp - let applicationContext = ApplicationContext { modelContext, session, autoRefreshServer, frameworkConfig, pgListener } + let applicationContext = ApplicationContext { modelContext, autoRefreshServer, frameworkConfig, pgListener } let toolServerApplication = ToolServerApplication { devServerContext = ?context } let application :: Wai.Application = \request respond -> do let ?applicationContext = applicationContext - requestContext <- ControllerSupport.createRequestContext applicationContext request respond - let ?context = requestContext - frontControllerToWAIApp toolServerApplication [] (staticApp request respond) + frontControllerToWAIApp @ToolServerApplication @AutoRefresh.AutoRefreshWSApp (\app -> app) toolServerApplication staticApp request respond let openAppUrl = openUrl ("http://localhost:" <> tshow port <> "/") let warpSettings = Warp.defaultSettings @@ -157,4 +154,4 @@ readDatabaseNeedsMigration :: (?context :: ControllerContext) => IO Bool readDatabaseNeedsMigration = do context <- theDevServerContext state <- readIORef (context.appStateRef) - readIORef (state.databaseNeedsMigration) + readIORef (state.databaseNeedsMigration) \ No newline at end of file diff --git a/IHP/RouterSupport.hs b/IHP/RouterSupport.hs index 2e88ea0be..96da10814 100644 --- a/IHP/RouterSupport.hs +++ b/IHP/RouterSupport.hs @@ -84,37 +84,38 @@ runAction' :: forall application controller . ( Controller controller , ?applicationContext :: ApplicationContext - , ?context :: RequestContext , InitControllerContext application , ?application :: application , Typeable application , Typeable controller ) - => controller -> (TMap.TMap -> TMap.TMap) -> IO ResponseReceived -runAction' controller contextSetter = do + => controller -> (TMap.TMap -> TMap.TMap) -> Application +runAction' controller contextSetter request respond = do let ?modelContext = ApplicationContext.modelContext ?applicationContext - let ?requestContext = ?context + requestContext <- createRequestContext ?applicationContext request respond + let ?context = requestContext + let ?requestContext = requestContext contextOrErrorResponse <- newContextForAction contextSetter controller case contextOrErrorResponse of Left res -> res Right context -> let ?context = context in runAction controller {-# INLINABLE runAction' #-} -type RouteParseResult = IO (TMap.TMap -> TMap.TMap, (TMap.TMap -> TMap.TMap) -> IO ResponseReceived) +type RouteParseResult = IO (TMap.TMap -> TMap.TMap, (TMap.TMap -> TMap.TMap) -> Application) type RouteParser = Parser (RouteParseResult) -toRouteParser :: Parser (IO ResponseReceived) -> RouteParser +toRouteParser :: Parser Application -> RouteParser toRouteParser parser = do controller <- parser pure $ pure (\t -> t, \_ -> controller) -toRouteParser' :: Parser ((TMap.TMap -> TMap.TMap) -> IO ResponseReceived) -> RouteParser +toRouteParser' :: Parser ((TMap.TMap -> TMap.TMap) -> Application) -> RouteParser toRouteParser' parser = do controller <- parser pure $ pure (\t -> t, controller) -toRouteParseResult :: IO ResponseReceived -> RouteParseResult -toRouteParseResult ioResponseReceived = pure (\t -> t, \_ -> ioResponseReceived) +toRouteParseResult :: Application -> RouteParseResult +toRouteParseResult application = pure (\t -> t, \_ -> application) class FrontController application where controllers @@ -132,8 +133,8 @@ defaultRouter => [RouteParser] -> RouteParser defaultRouter additionalControllers = do let allControllers = controllers <> additionalControllers - ioResponseReceived <- choice $ map (\r -> r <* endOfInput) allControllers - pure ioResponseReceived + applications <- choice $ map (\r -> r <* endOfInput) allControllers + pure applications {-# INLINABLE defaultRouter #-} class HasPath controller where @@ -835,13 +836,20 @@ startPage action = get (ByteString.pack (actionPrefix @action)) action withPrefix prefix routes = string prefix >> choice (map (\r -> r <* endOfInput) routes) {-# INLINABLE withPrefix #-} -runApp :: (?applicationContext :: ApplicationContext, ?context :: RequestContext) => RouteParser -> IO ResponseReceived -> IO ResponseReceived -runApp routes notFoundAction = do - let path = ?context.request.rawPathInfo - handleException :: SomeException -> IO (Either String (IO ResponseReceived)) +frontControllerToWAIApp :: forall app (autoRefreshApp :: Type). (?applicationContext :: ApplicationContext, FrontController app, WSApp autoRefreshApp, Typeable autoRefreshApp, InitControllerContext ()) => Middleware -> app -> Application -> Application +frontControllerToWAIApp middleware application notFoundAction request respond = do + let requestContext = RequestContext { request, respond, requestBody = FormBody { params = [], files = [] }, frameworkConfig = ?applicationContext.frameworkConfig } + + let ?context = requestContext + + let + path = request.rawPathInfo + handleException :: SomeException -> IO (Either String Application) handleException exception = pure $ Right $ ErrorController.handleRouterException exception - routedAction :: Either String (IO ResponseReceived) <- + routes = let ?application = application in router [let ?application = () in webSocketApp @autoRefreshApp] + + routedAction :: Either String Application <- (do res <- evaluate $ parseOnly (routes <* endOfInput) path case res of @@ -850,17 +858,10 @@ runApp routes notFoundAction = do (tmapSetter, controllerFn) <- io pure $ Right $ controllerFn $ tmapSetter ) - -- pure (undefined::IO ResponseReceived))) `Exception.catch` handleException case routedAction of - Left message -> notFoundAction - Right action -> action -{-# INLINABLE runApp #-} - -frontControllerToWAIApp :: forall app. (?applicationContext :: ApplicationContext, ?context :: RequestContext, FrontController app) => app -> [RouteParser] -> IO ResponseReceived -> IO ResponseReceived -frontControllerToWAIApp application additionalControllers notFoundAction = runApp defaultRouter notFoundAction - where - defaultRouter :: RouteParser = (let ?application = application in router additionalControllers) + Left message -> notFoundAction request respond + Right action -> (middleware action) request respond {-# INLINABLE frontControllerToWAIApp #-} mountFrontController :: forall frontController. (?applicationContext :: ApplicationContext, ?context :: RequestContext, FrontController frontController) => frontController -> RouteParser diff --git a/IHP/Server.hs b/IHP/Server.hs index ed93305a6..3899c4a5a 100644 --- a/IHP/Server.hs +++ b/IHP/Server.hs @@ -5,18 +5,16 @@ import IHP.Prelude import qualified Network.Wai.Handler.Warp as Warp import Network.Wai import Network.Wai.Middleware.MethodOverridePost (methodOverridePost) -import Network.Wai.Session (withSession, Session) +import Network.Wai.Session (withSession) import Network.Wai.Session.ClientSession (clientsessionStore) import qualified Web.ClientSession as ClientSession -import qualified Data.Vault.Lazy as Vault +import IHP.Controller.Session (sessionVaultKey) import IHP.ApplicationContext -import qualified IHP.ControllerSupport as ControllerSupport import qualified IHP.Environment as Env import qualified IHP.PGListener as PGListener import IHP.FrameworkConfig -import IHP.RouterSupport (frontControllerToWAIApp, FrontController, webSocketApp, webSocketAppWithCustomPath) -import IHP.ErrorController +import IHP.RouterSupport (frontControllerToWAIApp, FrontController) import qualified IHP.AutoRefresh as AutoRefresh import qualified IHP.AutoRefresh.Types as AutoRefresh import IHP.LibDir @@ -48,14 +46,12 @@ run configBuilder = do withInitalizers frameworkConfig modelContext do withPGListener \pgListener -> do - sessionVault <- Vault.newKey - autoRefreshServer <- newIORef (AutoRefresh.newAutoRefreshServer pgListener) let ?modelContext = modelContext - let ?applicationContext = ApplicationContext { modelContext = ?modelContext, session = sessionVault, autoRefreshServer, frameworkConfig, pgListener } + let ?applicationContext = ApplicationContext { modelContext = ?modelContext, autoRefreshServer, frameworkConfig, pgListener } - sessionMiddleware <- initSessionMiddleware sessionVault frameworkConfig + sessionMiddleware <- initSessionMiddleware frameworkConfig staticApp <- initStaticApp frameworkConfig let corsMiddleware = initCorsMiddleware frameworkConfig let requestLoggerMiddleware = frameworkConfig.requestLoggerMiddleware @@ -65,10 +61,9 @@ run configBuilder = do . runServer frameworkConfig . customMiddleware . corsMiddleware - . sessionMiddleware - . requestLoggerMiddleware . methodOverridePost - $ application staticApp + . sessionMiddleware + $ application staticApp requestLoggerMiddleware {-# INLINABLE run #-} @@ -99,7 +94,7 @@ initStaticApp frameworkConfig = do frameworkStaticDir = libDir <> "/static/" frameworkSettings = (Static.defaultWebAppSettings frameworkStaticDir) - { Static.ss404Handler = Just handleNotFound + { Static.ss404Handler = Just (frameworkConfig.requestLoggerMiddleware handleNotFound) , Static.ssMaxAge = maxAge } appSettings = (Static.defaultWebAppSettings "static/") @@ -109,8 +104,8 @@ initStaticApp frameworkConfig = do pure (Static.staticApp appSettings) -initSessionMiddleware :: Vault.Key (Session IO ByteString ByteString) -> FrameworkConfig -> IO Middleware -initSessionMiddleware sessionVault FrameworkConfig { sessionCookie } = do +initSessionMiddleware :: FrameworkConfig -> IO Middleware +initSessionMiddleware FrameworkConfig { sessionCookie } = do let path = "Config/client_session_key.aes" hasSessionSecretEnvVar <- EnvVar.hasEnvVar "IHP_SESSION_SECRET" @@ -119,7 +114,7 @@ initSessionMiddleware sessionVault FrameworkConfig { sessionCookie } = do if hasSessionSecretEnvVar || not doesConfigDirectoryExist then ClientSession.getKeyEnv "IHP_SESSION_SECRET" else ClientSession.getKey path - let sessionMiddleware :: Middleware = withSession store "SESSION" sessionCookie sessionVault + let sessionMiddleware :: Middleware = withSession store "SESSION" sessionCookie sessionVaultKey pure sessionMiddleware initCorsMiddleware :: FrameworkConfig -> Middleware @@ -127,16 +122,9 @@ initCorsMiddleware FrameworkConfig { corsResourcePolicy } = case corsResourcePol Just corsResourcePolicy -> Cors.cors (const (Just corsResourcePolicy)) Nothing -> id -application :: (FrontController RootApplication, ?applicationContext :: ApplicationContext) => Application -> Application -application staticApp request respond = do - requestContext <- ControllerSupport.createRequestContext ?applicationContext request respond - let ?context = requestContext - let builtinControllers = let ?application = () in - [ webSocketApp @AutoRefresh.AutoRefreshWSApp - , webSocketAppWithCustomPath @AutoRefresh.AutoRefreshWSApp "" -- For b.c. with older versions of ihp-auto-refresh.js - ] - - frontControllerToWAIApp RootApplication builtinControllers (staticApp request respond) +application :: (FrontController RootApplication, ?applicationContext :: ApplicationContext) => Application -> Middleware -> Application +application staticApp middleware request respond = do + frontControllerToWAIApp @RootApplication @AutoRefresh.AutoRefreshWSApp middleware RootApplication staticApp request respond {-# INLINABLE application #-} runServer :: (?applicationContext :: ApplicationContext) => FrameworkConfig -> Application -> IO () @@ -152,9 +140,6 @@ runServer FrameworkConfig { environment = Env.Production, appPort, exceptionTrac |> Warp.setPort appPort |> Warp.setOnException exceptionTracker.onException -instance ControllerSupport.InitControllerContext () where - initContext = pure () - withInitalizers :: FrameworkConfig -> ModelContext -> IO () -> IO () withInitalizers frameworkConfig modelContext continue = do let ?context = frameworkConfig diff --git a/IHP/Test/Mocking.hs b/IHP/Test/Mocking.hs index 0598b2abd..8582cf66a 100644 --- a/IHP/Test/Mocking.hs +++ b/IHP/Test/Mocking.hs @@ -33,6 +33,7 @@ import qualified Network.Wai.Session import qualified Data.Serialize as Serialize import qualified Control.Exception as Exception import qualified IHP.PGListener as PGListener +import IHP.Controller.Session (sessionVaultKey) type ContextParameters application = (?applicationContext :: ApplicationContext, ?context :: RequestContext, ?modelContext :: ModelContext, ?application :: application, InitControllerContext application, ?mocking :: MockContext application) @@ -58,17 +59,15 @@ withIHPApp application configBuilder hspecAction = do withTestDatabase \testDatabase -> do modelContext <- createModelContext dbPoolIdleTime dbPoolMaxConnections (testDatabase.url) logger - session <- Vault.newKey pgListener <- PGListener.init modelContext autoRefreshServer <- newIORef (AutoRefresh.newAutoRefreshServer pgListener) - let sessionVault = Vault.insert session mempty Vault.empty - let applicationContext = ApplicationContext { modelContext = modelContext, session, autoRefreshServer, frameworkConfig, pgListener } + let sessionVault = Vault.insert sessionVaultKey mempty Vault.empty + let applicationContext = ApplicationContext { modelContext = modelContext, autoRefreshServer, frameworkConfig, pgListener } let requestContext = RequestContext { request = defaultRequest {vault = sessionVault} , requestBody = FormBody [] [] , respond = const (pure ResponseReceived) - , vault = session , frameworkConfig = frameworkConfig } (hspecAction MockContext { .. }) @@ -81,17 +80,15 @@ mockContextNoDatabase application configBuilder = do logger <- newLogger def { level = Warn } -- don't log queries modelContext <- createModelContext dbPoolIdleTime dbPoolMaxConnections databaseUrl logger - session <- Vault.newKey - let sessionVault = Vault.insert session mempty Vault.empty + let sessionVault = Vault.insert sessionVaultKey mempty Vault.empty pgListener <- PGListener.init modelContext autoRefreshServer <- newIORef (AutoRefresh.newAutoRefreshServer pgListener) - let applicationContext = ApplicationContext { modelContext = modelContext, session, autoRefreshServer, frameworkConfig, pgListener } + let applicationContext = ApplicationContext { modelContext = modelContext, autoRefreshServer, frameworkConfig, pgListener } let requestContext = RequestContext { request = defaultRequest {vault = sessionVault} , requestBody = FormBody [] [] , respond = \resp -> pure ResponseReceived - , vault = session , frameworkConfig = frameworkConfig } pure MockContext{..} @@ -230,8 +227,8 @@ withUser user callback = insertSession key value = pure () - newVault = Vault.insert vaultKey newSession (Wai.vault request) - RequestContext { request, vault = vaultKey } = ?mocking.requestContext + newVault = Vault.insert sessionVaultKey newSession (Wai.vault request) + RequestContext { request } = ?mocking.requestContext sessionValue = Serialize.encode (user.id) sessionKey = cs (Session.sessionKey @user) diff --git a/Main.hs b/Main.hs index 0442bc236..0775466b4 100644 --- a/Main.hs +++ b/Main.hs @@ -13,12 +13,19 @@ data DemoController = DemoAction deriving (Eq, Show, Data) instance AutoRoute DemoController instance InitControllerContext RootApplication -instance FrontController RootApplication where +data WebApplication = WebApplication deriving (Eq, Show) + +instance InitControllerContext WebApplication where + initContext = pure () + +instance FrontController WebApplication where controllers = - [ parseRoute @DemoController - , startPage DemoAction + [ startPage DemoAction ] +instance FrontController RootApplication where + controllers = [ mountFrontController WebApplication ] + instance Controller DemoController where action DemoAction = renderPlain "Hello World!" diff --git a/NixSupport/default.nix b/NixSupport/default.nix index 5a0fa1975..50a8bc71d 100644 --- a/NixSupport/default.nix +++ b/NixSupport/default.nix @@ -1,4 +1,4 @@ -{ compiler ? "ghc94" +{ compiler ? "ghc96" , additionalNixpkgsOptions ? {} , pkgs ? import "${toString projectPath}/Config/nix/nixpkgs-config.nix" { ihp = ihp; additionalNixpkgsOptions = additionalNixpkgsOptions; } , ghc ? pkgs.haskell.packages.${compiler} diff --git a/NixSupport/haskell-packages/minio-hs.nix b/NixSupport/haskell-packages/minio-hs.nix new file mode 100644 index 000000000..d2322f623 --- /dev/null +++ b/NixSupport/haskell-packages/minio-hs.nix @@ -0,0 +1,41 @@ +{ mkDerivation, aeson, base, base64-bytestring, binary, bytestring +, case-insensitive, conduit, conduit-extra, crypton-connection +, cryptonite, cryptonite-conduit, digest, directory, filepath +, http-client, http-client-tls, http-conduit, http-types, ini, lib +, memory, network-uri, QuickCheck, raw-strings-qq, relude +, resourcet, retry, tasty, tasty-hunit, tasty-quickcheck +, tasty-smallcheck, text, time, time-units, transformers, unliftio +, unliftio-core, unordered-containers, xml-conduit, fetchFromGitHub +}: +mkDerivation { + pname = "minio-hs"; + version = "1.7.0"; + src = fetchFromGitHub { + owner = "mpscholten"; + repo = "minio-hs"; + rev = "786cf1881f0b62b7539e63547e76afc3c1ade36a"; + sha256 = "sha256-dK4nH6j16oFNB27tp1ExAWahhFUlKpzLpqTiHju8dl8="; + }; + isLibrary = true; + isExecutable = true; + libraryHaskellDepends = [ + aeson base base64-bytestring binary bytestring case-insensitive + conduit conduit-extra crypton-connection cryptonite + cryptonite-conduit digest directory filepath http-client + http-client-tls http-conduit http-types ini memory network-uri + relude resourcet retry text time time-units transformers unliftio + unliftio-core unordered-containers xml-conduit + ]; + testHaskellDepends = [ + aeson base base64-bytestring binary bytestring case-insensitive + conduit conduit-extra crypton-connection cryptonite + cryptonite-conduit digest directory filepath http-client + http-client-tls http-conduit http-types ini memory network-uri + QuickCheck raw-strings-qq relude resourcet retry tasty tasty-hunit + tasty-quickcheck tasty-smallcheck text time time-units transformers + unliftio unliftio-core unordered-containers xml-conduit + ]; + homepage = "https://github.com/minio/minio-hs#readme"; + description = "A MinIO Haskell Library for Amazon S3 compatible cloud storage"; + license = lib.licenses.asl20; +} \ No newline at end of file diff --git a/NixSupport/haskell-packages/smtp-mail.nix b/NixSupport/haskell-packages/smtp-mail.nix new file mode 100644 index 000000000..56c141158 --- /dev/null +++ b/NixSupport/haskell-packages/smtp-mail.nix @@ -0,0 +1,22 @@ +{ mkDerivation, array, base, base16-bytestring, base64-bytestring +, bytestring, crypton-connection, cryptonite, filepath, lib, memory +, mime-mail, network, network-bsd, text, fetchFromGitHub +}: +mkDerivation { + pname = "smtp-mail"; + version = "0.3.0.0"; + src = fetchFromGitHub { + owner = "MasterWordServices"; + repo = "smtp-mail"; + rev = "4c724c80814ab1da7c37256a6c10e04c88b9af95"; + sha256 = "sha256-gjg2k8UHBwumoH8OUsFZ+Xd5icotaVHRmlQt6Xc7Mf0="; + }; + libraryHaskellDepends = [ + array base base16-bytestring base64-bytestring bytestring + crypton-connection cryptonite filepath memory mime-mail network network-bsd + text + ]; + homepage = "http://github.com/jhickner/smtp-mail"; + description = "Simple email sending via SMTP"; + license = lib.licenses.bsd3; +} \ No newline at end of file diff --git a/NixSupport/make-nixpkgs-from-options.nix b/NixSupport/make-nixpkgs-from-options.nix index ff6551f27..5c3517906 100644 --- a/NixSupport/make-nixpkgs-from-options.nix +++ b/NixSupport/make-nixpkgs-from-options.nix @@ -8,7 +8,7 @@ , dontHaddockPackages ? [] , nixPkgsRev ? "277bf961c323b6cde46932cc9308135d0687af95" , nixPkgsSha256 ? "sha256-ciSZqliNLDattmjR/1yQ0mJHZSkXWZlnIR6TcdDGFMQ" -, compiler ? "ghc94" +, compiler ? "ghc96" , manualOverrides ? haskellPackagesNew: haskellPackagesOld: { } # More exotic overrides go here , additionalNixpkgsOptions ? {} }: diff --git a/Test/Controller/AccessDeniedSpec.hs b/Test/Controller/AccessDeniedSpec.hs index 8a847540c..dba55e502 100644 --- a/Test/Controller/AccessDeniedSpec.hs +++ b/Test/Controller/AccessDeniedSpec.hs @@ -74,8 +74,8 @@ config = do makeApplication :: (?applicationContext :: ApplicationContext) => IO Application makeApplication = do store <- Session.mapStore_ - let sessionMiddleware :: Middleware = Session.withSession store "SESSION" ?applicationContext.frameworkConfig.sessionCookie ?applicationContext.session - pure (sessionMiddleware (Server.application handleNotFound)) + let sessionMiddleware :: Middleware = Session.withSession store "SESSION" ?applicationContext.frameworkConfig.sessionCookie sessionVaultKey + pure (sessionMiddleware $ (Server.application handleNotFound) (\app -> app)) assertAccessDenied :: SResponse -> IO () assertAccessDenied response = do diff --git a/Test/Controller/CookieSpec.hs b/Test/Controller/CookieSpec.hs index ca950a556..8750b175e 100644 --- a/Test/Controller/CookieSpec.hs +++ b/Test/Controller/CookieSpec.hs @@ -37,6 +37,6 @@ createControllerContext = do let requestBody = FormBody { params = [], files = [] } request = Wai.defaultRequest - requestContext = RequestContext { request, respond = error "respond", requestBody, vault = error "vault", frameworkConfig = error "frameworkConfig" } + requestContext = RequestContext { request, respond = error "respond", requestBody, frameworkConfig = error "frameworkConfig" } let ?requestContext = requestContext newControllerContext diff --git a/Test/Controller/NotFoundSpec.hs b/Test/Controller/NotFoundSpec.hs index 2c526db25..b79ca3cd3 100644 --- a/Test/Controller/NotFoundSpec.hs +++ b/Test/Controller/NotFoundSpec.hs @@ -74,8 +74,8 @@ config = do makeApplication :: (?applicationContext :: ApplicationContext) => IO Application makeApplication = do store <- Session.mapStore_ - let sessionMiddleware :: Middleware = Session.withSession store "SESSION" ?applicationContext.frameworkConfig.sessionCookie ?applicationContext.session - pure (sessionMiddleware (Server.application handleNotFound)) + let sessionMiddleware :: Middleware = Session.withSession store "SESSION" ?applicationContext.frameworkConfig.sessionCookie sessionVaultKey + pure (sessionMiddleware $ (Server.application handleNotFound) (\app -> app)) assertNotFound :: SResponse -> IO () assertNotFound response = do diff --git a/Test/Controller/ParamSpec.hs b/Test/Controller/ParamSpec.hs index 630ad13c0..d45ed7c00 100644 --- a/Test/Controller/ParamSpec.hs +++ b/Test/Controller/ParamSpec.hs @@ -434,14 +434,14 @@ createControllerContextWithParams params = let requestBody = FormBody { params, files = [] } request = Wai.defaultRequest - requestContext = RequestContext { request, respond = error "respond", requestBody, vault = error "vault", frameworkConfig = error "frameworkConfig" } + requestContext = RequestContext { request, respond = error "respond", requestBody, frameworkConfig = error "frameworkConfig" } in FrozenControllerContext { requestContext, customFields = TypeMap.empty } createControllerContextWithJson params = let requestBody = JSONBody { jsonPayload = Just (json params), rawPayload = cs params } request = Wai.defaultRequest - requestContext = RequestContext { request, respond = error "respond", requestBody, vault = error "vault", frameworkConfig = error "frameworkConfig" } + requestContext = RequestContext { request, respond = error "respond", requestBody, frameworkConfig = error "frameworkConfig" } in FrozenControllerContext { requestContext, customFields = TypeMap.empty } json :: Text -> Aeson.Value diff --git a/Test/RouterSupportSpec.hs b/Test/RouterSupportSpec.hs index 09a4c0158..bac750f42 100644 --- a/Test/RouterSupportSpec.hs +++ b/Test/RouterSupportSpec.hs @@ -141,7 +141,7 @@ config = do option (AppPort 8000) application :: (?applicationContext :: ApplicationContext) => Application -application = Server.application handleNotFound +application = Server.application handleNotFound (\app -> app) tests :: Spec tests = beforeAll (mockContextNoDatabase WebApplication config) do diff --git a/Test/SEO/Sitemap.hs b/Test/SEO/Sitemap.hs index 82155b618..ed85c1d1b 100644 --- a/Test/SEO/Sitemap.hs +++ b/Test/SEO/Sitemap.hs @@ -79,5 +79,5 @@ tests = beforeAll (mockContextNoDatabase WebApplication config) do describe "SEO" do describe "Sitemap" do it "should render a XML Sitemap" $ withContext do - runSession (testGet "/sitemap.xml") (Server.application handleNotFound) + runSession (testGet "/sitemap.xml") (Server.application handleNotFound (\app -> app)) >>= assertSuccess "http://localhost:8000/test/ShowPost?postId=00000000-0000-0000-0000-0000000000002105-04-16hourly" diff --git a/Test/View/CSSFrameworkSpec.hs b/Test/View/CSSFrameworkSpec.hs index 11f1a2c24..945defe00 100644 --- a/Test/View/CSSFrameworkSpec.hs +++ b/Test/View/CSSFrameworkSpec.hs @@ -721,5 +721,5 @@ createControllerContextWithCSSFramework cssFramework = do option cssFramework let requestBody = FormBody { params = [], files = [] } let request = Wai.defaultRequest - let requestContext = RequestContext { request, respond = error "respond", requestBody, vault = error "vault", frameworkConfig = frameworkConfig } + let requestContext = RequestContext { request, respond = error "respond", requestBody, frameworkConfig = frameworkConfig } pure FrozenControllerContext { requestContext, customFields = TypeMap.empty } \ No newline at end of file diff --git a/Test/View/FormSpec.hs b/Test/View/FormSpec.hs index 7cc5d27bb..75532c411 100644 --- a/Test/View/FormSpec.hs +++ b/Test/View/FormSpec.hs @@ -49,7 +49,7 @@ createControllerContext = do frameworkConfig <- FrameworkConfig.buildFrameworkConfig (pure ()) let requestBody = FormBody { params = [], files = [] } let request = Wai.defaultRequest - let requestContext = RequestContext { request, respond = undefined, requestBody, vault = undefined, frameworkConfig = frameworkConfig } + let requestContext = RequestContext { request, respond = undefined, requestBody, frameworkConfig = frameworkConfig } pure FrozenControllerContext { requestContext, customFields = mempty } data Project' = Project {id :: (Id' "projects"), title :: Text, meta :: MetaBag} deriving (Eq, Show) diff --git a/Test/ViewSupportSpec.hs b/Test/ViewSupportSpec.hs index 8911230cd..191bda6e8 100644 --- a/Test/ViewSupportSpec.hs +++ b/Test/ViewSupportSpec.hs @@ -101,8 +101,8 @@ config = do makeApplication :: (?applicationContext :: ApplicationContext) => IO Application makeApplication = do store <- Session.mapStore_ - let sessionMiddleware :: Middleware = Session.withSession store "SESSION" ?applicationContext.frameworkConfig.sessionCookie ?applicationContext.session - pure (sessionMiddleware (Server.application handleNotFound)) + let sessionMiddleware :: Middleware = Session.withSession store "SESSION" ?applicationContext.frameworkConfig.sessionCookie sessionVaultKey + pure (sessionMiddleware $ (Server.application handleNotFound (\app -> app))) tests :: Spec tests = beforeAll (mockContextNoDatabase WebApplication config) do diff --git a/devenv-module.nix b/devenv-module.nix index 0086532e2..020e75013 100644 --- a/devenv-module.nix +++ b/devenv-module.nix @@ -8,7 +8,7 @@ that is defined in flake-module.nix perSystem = { nix-filter, pkgs, lib, ... }: let ghcCompiler = import ./NixSupport/mkGhcCompiler.nix { inherit pkgs; - ghcCompiler = pkgs.haskell.packages.ghc94; + ghcCompiler = pkgs.haskell.packages.ghc96; ihp = ./.; filter = inputs.nix-filter.lib; }; diff --git a/flake-module.nix b/flake-module.nix index 338894378..507f62bc8 100644 --- a/flake-module.nix +++ b/flake-module.nix @@ -26,7 +26,7 @@ ihpFlake: description = '' The GHC compiler to use for IHP. ''; - default = pkgs.haskell.packages.ghc94; + default = pkgs.haskell.packages.ghc96; }; packages = lib.mkOption { diff --git a/flake.lock b/flake.lock index 5a1756a44..2e7092209 100644 --- a/flake.lock +++ b/flake.lock @@ -488,17 +488,17 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1696291921, - "narHash": "sha256-isKgVAoUxuxYEuO3Q4xhbfKcZrF/+UkJtOTv0eb/W5E=", + "lastModified": 1710461556, + "narHash": "sha256-uxBpjpYSVLCkNxOLHDuli2twd6mmuvuTuU77aT/eiGY=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "ea0284a3da391822909be5e98a60c1e62572a7dc", + "rev": "cb97fed5722648882d1b4a03df0b67681909fb68", "type": "github" }, "original": { "owner": "NixOS", "repo": "nixpkgs", - "rev": "ea0284a3da391822909be5e98a60c1e62572a7dc", + "rev": "cb97fed5722648882d1b4a03df0b67681909fb68", "type": "github" } }, diff --git a/flake.nix b/flake.nix index bed7dd964..88e4c7138 100644 --- a/flake.nix +++ b/flake.nix @@ -3,7 +3,7 @@ inputs = { # TODO use nixpkgs-unstable and just .lock a version? - nixpkgs.url = "github:NixOS/nixpkgs?rev=ea0284a3da391822909be5e98a60c1e62572a7dc"; + nixpkgs.url = "github:NixOS/nixpkgs?rev=cb97fed5722648882d1b4a03df0b67681909fb68"; # pre-defined set of default target systems systems.url = "github:nix-systems/default"; diff --git a/ihp-hsx/IHP/HSX/HaskellParser.hs b/ihp-hsx/IHP/HSX/HaskellParser.hs index 1895d8fa7..08f93e69b 100644 --- a/ihp-hsx/IHP/HSX/HaskellParser.hs +++ b/ihp-hsx/IHP/HSX/HaskellParser.hs @@ -30,7 +30,7 @@ parseHaskellExpression sourcePos extensions input = error = renderWithContext defaultSDocContext $ vcat $ map (formatBulleted defaultSDocContext) -#if __GLASGOW_HASKELL__ >= 960 +#if __GLASGOW_HASKELL__ >= 906 $ map (diagnosticMessage NoDiagnosticOpts) #else $ map diagnosticMessage diff --git a/ihp-hsx/IHP/HSX/HsExpToTH.hs b/ihp-hsx/IHP/HSX/HsExpToTH.hs index d7a33e6d0..35ed01fff 100644 --- a/ihp-hsx/IHP/HSX/HsExpToTH.hs +++ b/ihp-hsx/IHP/HSX/HsExpToTH.hs @@ -28,7 +28,7 @@ import qualified GHC.Unit.Module as Module import GHC.Stack import qualified Data.List.NonEmpty as NonEmpty import Language.Haskell.Syntax.Type -#if __GLASGOW_HASKELL__ >= 960 +#if __GLASGOW_HASKELL__ >= 906 import Language.Haskell.Syntax.Basic #endif @@ -92,7 +92,7 @@ toExp (Expr.HsVar _ n) = then TH.ConE (toName n') else TH.VarE (toName n') -#if __GLASGOW_HASKELL__ >= 960 +#if __GLASGOW_HASKELL__ >= 906 toExp (Expr.HsUnboundVar _ n) = TH.UnboundVarE (TH.mkName . occNameString $ occName n) #else toExp (Expr.HsUnboundVar _ n) = TH.UnboundVarE (TH.mkName . occNameString $ n) @@ -110,7 +110,7 @@ toExp (Expr.HsOverLit _ OverLit {ol_val}) toExp (Expr.HsApp _ e1 e2) = TH.AppE (toExp . unLoc $ e1) (toExp . unLoc $ e2) -#if __GLASGOW_HASKELL__ >= 960 +#if __GLASGOW_HASKELL__ >= 906 toExp (Expr.HsAppType _ e _ HsWC {hswc_body}) = TH.AppTypeE (toExp . unLoc $ e) (toType . unLoc $ hswc_body) #else toExp (Expr.HsAppType _ e HsWC {hswc_body}) = TH.AppTypeE (toExp . unLoc $ e) (toType . unLoc $ hswc_body) @@ -124,7 +124,7 @@ toExp (Expr.NegApp _ e _) = TH.AppE (TH.VarE 'negate) (toExp . unLoc $ e) -- NOTE: for lambda, there is only one match -#if __GLASGOW_HASKELL__ >= 960 +#if __GLASGOW_HASKELL__ >= 906 toExp (Expr.HsLam _ (Expr.MG _ (unLoc -> (map unLoc -> [Expr.Match _ _ (map unLoc -> ps) (Expr.GRHSs _ [unLoc -> Expr.GRHS _ _ (unLoc -> e)] _)])))) #else toExp (Expr.HsLam _ (Expr.MG _ (unLoc -> (map unLoc -> [Expr.Match _ _ (map unLoc -> ps) (Expr.GRHSs _ [unLoc -> Expr.GRHS _ _ (unLoc -> e)] _)])) _)) @@ -199,7 +199,7 @@ toExp (Expr.HsProjection _ locatedFields) = extractFieldLabel (DotFieldOcc _ locatedStr) = locatedStr extractFieldLabel _ = error "Don't know how to handle XDotFieldOcc constructor..." in -#if __GLASGOW_HASKELL__ >= 960 +#if __GLASGOW_HASKELL__ >= 906 TH.ProjectionE (NonEmpty.map (unpackFS . (.field_label) . unLoc . extractFieldLabel . unLoc) locatedFields) #else TH.ProjectionE (NonEmpty.map (unpackFS . unLoc . extractFieldLabel . unLoc) locatedFields) @@ -210,13 +210,13 @@ toExp (Expr.HsGetField _ expr locatedField) = extractFieldLabel (DotFieldOcc _ locatedStr) = locatedStr extractFieldLabel _ = error "Don't know how to handle XDotFieldOcc constructor..." in -#if __GLASGOW_HASKELL__ >= 960 +#if __GLASGOW_HASKELL__ >= 906 TH.GetFieldE (toExp (unLoc expr)) (unpackFS . (.field_label) . unLoc . extractFieldLabel . unLoc $ locatedField) #else TH.GetFieldE (toExp (unLoc expr)) (unpackFS . unLoc . extractFieldLabel . unLoc $ locatedField) #endif -#if __GLASGOW_HASKELL__ >= 960 +#if __GLASGOW_HASKELL__ >= 906 toExp (Expr.HsOverLabel _ _ fastString) = TH.LabelE (unpackFS fastString) #else toExp (Expr.HsOverLabel _ fastString) = TH.LabelE (unpackFS fastString) diff --git a/ihp-hsx/shell.nix b/ihp-hsx/shell.nix index f74a66058..cb3f12e2a 100644 --- a/ihp-hsx/shell.nix +++ b/ihp-hsx/shell.nix @@ -3,7 +3,7 @@ let ihp = ./../.; haskellPackagesDir = ./../NixSupport/haskell-packages; }; - ghc = pkgs.haskell.packages.ghc94; + ghc = pkgs.haskell.packages.ghc96; haskellDeps = ghc.ghcWithPackages (p: with p; [ classy-prelude string-conversions diff --git a/ihp-openai/IHP/OpenAI.hs b/ihp-openai/IHP/OpenAI.hs index f5ba2daf1..69f23f94d 100644 --- a/ihp-openai/IHP/OpenAI.hs +++ b/ihp-openai/IHP/OpenAI.hs @@ -19,7 +19,7 @@ import Control.Applicative ((<|>)) data CompletionRequest = CompletionRequest { messages :: ![Message] - , prompt :: !Text + , prompt :: !Text -- ^ Deprecated, use 'messages' instead , maxTokens :: !Int , temperature :: !Double , presencePenalty :: !Double @@ -39,7 +39,7 @@ instance ToJSON CompletionRequest where toJSON CompletionRequest { model, prompt, messages, maxTokens, temperature, presencePenalty, frequencePenalty, stream } = object [ "model" .= model - , "messages" .= (messages <> [userMessage prompt]) + , "messages" .= (messages <> (if not (Text.null prompt) then [userMessage prompt] else [])) , "max_tokens" .= maxTokens , "stream" .= stream , "temperature" .= temperature @@ -62,6 +62,9 @@ userMessage content = Message { role = UserRole, content } systemMessage :: Text -> Message systemMessage content = Message { role = SystemRole, content } +assistantMessage :: Text -> Message +assistantMessage content = Message { role = AssistantRole, content } + newCompletionRequest :: CompletionRequest newCompletionRequest = CompletionRequest { prompt = "" diff --git a/ihp-openai/shell.nix b/ihp-openai/shell.nix index 926699d11..197cf9b06 100644 --- a/ihp-openai/shell.nix +++ b/ihp-openai/shell.nix @@ -3,7 +3,7 @@ let ihp = ./../.; haskellPackagesDir = ./../NixSupport/haskell-packages; }; - ghc = pkgs.haskell.packages.ghc94; + ghc = pkgs.haskell.packages.ghc96; haskellDeps = ghc.ghcWithPackages (p: with p; [ text bytestring diff --git a/lib/IHP/Makefile.dist b/lib/IHP/Makefile.dist index 6a3ca2890..ec3bd6f14 100644 --- a/lib/IHP/Makefile.dist +++ b/lib/IHP/Makefile.dist @@ -114,10 +114,10 @@ psql: ## Connects to the running postgresql server @psql -h $$PWD/build/db -d app db: Application/Schema.sql Application/Fixtures.sql ## Creates a new database with the current Schema and imports Fixtures.sql - echo "drop schema public cascade; create schema public;" | psql -h $$PWD/build/db -d app - psql -h $$PWD/build/db -d app < "${IHP_LIB}/IHPSchema.sql" - psql -h $$PWD/build/db -d app < Application/Schema.sql - psql -h $$PWD/build/db -d app < Application/Fixtures.sql + (echo "drop schema public cascade; create schema public;" | psql -h $${PWD}/build/db -d app) && \ + psql -v ON_ERROR_STOP=1 -h $${PWD}/build/db -d app < "$${IHP_LIB}/IHPSchema.sql" && \ + psql -v ON_ERROR_STOP=1 -h $${PWD}/build/db -d app < Application/Schema.sql && \ + psql -v ON_ERROR_STOP=1 -h $${PWD}/build/db -d app < Application/Fixtures.sql dumpdb: dump_db ## Saves the current database state into the Fixtures.sql dump_db: ## Saves the current database state into the Fixtures.sql