├── .gitignore ├── .travis.yml ├── Changelog.md ├── LICENSE ├── README.md ├── elm.json ├── examples ├── Examples.elm └── elm.json ├── script └── watch ├── src └── Postgrest │ ├── Client.elm │ └── Internal │ ├── Endpoint.elm │ ├── JWT.elm │ ├── Params.elm │ ├── Requests.elm │ └── URL.elm └── tests └── PostgrestTests.elm /.gitignore: -------------------------------------------------------------------------------- 1 | elm-stuff/ 2 | .DS_Store 3 | docs.json 4 | .idea/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: elm 2 | elm: 3 | - elm0.19.0 4 | elm-test: 0.19.0-rev3 5 | elm-format: 0.8.3 6 | -------------------------------------------------------------------------------- /Changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 2.1.1 4 | 5 | - Add "Prefer" "return=representation" header to delete requests. This resolves an issue where defaultSelect was specified with customEndpoint and postgrest wouldn't accept select as a parameter. 6 | 7 | ## 2.1.0 8 | 9 | - Add support for `cs` and `cd` operators. 10 | 11 | ## 2.0.1 12 | 13 | - Change ilike and like parameters to not be quoted. (Otherwise it wouldn't work with 5.2.0) Previously it would look like `ilike."value*"` but now it's `ilike.value*`. 14 | 15 | ## 2.0.0 16 | 17 | - Adjust BadStatus type to include body string. This will be useful if postgrest responses change over time or if an HTTP service proxies to postgrest for the success case but returns a custom error response for the failure case. 18 | 19 | ## 1.0.0 20 | 21 | - Initial fork from alex-tan/postgrest-queries with new features to create HTTP requests. 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 Alex Tan 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # alex-tan/postgrest-client 2 | 3 | [![Build Status](https://travis-ci.org/alex-tan/postgrest-client.svg?branch=master)](https://travis-ci.org/alex-tan/postgrest-client) 4 | 5 | This package allows you to both easily construct [Postgrest query strings](http://postgrest.org/en/v5.1/api.html#horizontal-filtering-rows) and also make postgrest requests using Elm. 6 | 7 | This library allows you to construct and execute requests in a typesafe manner, with 8 | little boilerplate. Here's what a full example might look like: 9 | 10 | ```elm 11 | module Api.People exposing (delete, getMany, post) 12 | 13 | import Json.Decode exposing (..) 14 | import Json.Encode as JE 15 | import Postgrest.Client as P 16 | 17 | -- Optional, but recommended to have a type that 18 | -- represents your primary key. 19 | type PersonID 20 | = PersonID Int 21 | 22 | 23 | -- And a way to unwrap it... 24 | personID : PersonID -> Int 25 | personID (PersonID id) = 26 | id 27 | 28 | 29 | -- Define the record you would fetch back from the server. 30 | type alias Person = 31 | { id : PersonID 32 | , name : String 33 | } 34 | 35 | 36 | -- Define a submission record, without the primary key. 37 | type alias PersonSubmission = 38 | { name : String 39 | } 40 | 41 | 42 | -- Decoders are written using Json.Decode 43 | decodeUnit : Decoder Person 44 | decodeUnit = 45 | map2 Person 46 | (field "id" <| map PersonID int) 47 | (field "name" string) 48 | 49 | 50 | -- Encoders are written using Json.Encode 51 | encode : PersonSubmission -> JE.Value 52 | encode person = 53 | JE.object 54 | [ ( "name", JE.string person.name ) 55 | ] 56 | 57 | 58 | -- Tell Postgrest.Client the column name of your primary key and 59 | -- how to convert it into a parameter. 60 | primaryKey : P.PrimaryKey PersonID 61 | primaryKey = 62 | P.primaryKey ( "id", P.int << personID ) 63 | 64 | 65 | -- Tell Postgrest.Client the URL of the postgrest endpoint and how 66 | -- to decode records from it. 67 | endpoint : P.Endpoint Person 68 | endpoint = 69 | P.endpoint "/people" decodeUnit 70 | 71 | 72 | -- Fetch many records. If you want to specify parameters use `setParams` 73 | getMany : P.Request (List Person) 74 | getMany = 75 | P.getMany endpoint 76 | 77 | 78 | -- Delete by primary key. This is a convenience function that reduces 79 | -- the likelihood that you delete the wrong records by specifying incorrect 80 | -- parameters. 81 | delete : PersonID -> P.Request PersonID 82 | delete = 83 | P.deleteByPrimaryKey endpoint primaryKey 84 | 85 | 86 | -- Create a record. 87 | post : PersonSubmission -> P.Request Person 88 | post = 89 | P.postOne endpoint << encode 90 | ``` 91 | 92 | Here's how you could use it: 93 | 94 | ```elm 95 | import Api.People as People 96 | import Postgrest.Client as P 97 | 98 | jwt : P.JWT 99 | jwt = 100 | P.jwt "myjwt" 101 | 102 | type Msg 103 | = PersonCreated (Result P.Error Person) 104 | | PeopleLoaded (Result P.Error (List Person)) 105 | | PersonDeleted (Result P.Error PersonID) 106 | 107 | toCmd = 108 | P.toCmd jwt 109 | 110 | cmdExamples = 111 | [ People.post 112 | { name = "Yasujirō Ozu" 113 | } 114 | |> P.toCmd jwt PersonCreated 115 | , People.getMany 116 | |> P.setParams [ P.order [ P.asc "name" ], P.limit 10 ] 117 | |> toCmd PeopleLoaded 118 | , Person.delete personID 119 | |> toCmd PersonDeleted 120 | ] 121 | ``` 122 | 123 | Most query operators are currently supported: 124 | 125 | * [select](https://package.elm-lang.org/packages/alex-tan/postgrest-client/latest/Postgrest-Client#select) 126 | * [eq](https://package.elm-lang.org/packages/alex-tan/postgrest-client/latest/Postgrest-Client#eq) 127 | * [gt](https://package.elm-lang.org/packages/alex-tan/postgrest-client/latest/Postgrest-Client#gt) 128 | * [gte](https://package.elm-lang.org/packages/alex-tan/postgrest-client/latest/Postgrest-Client#gte) 129 | * [lt](https://package.elm-lang.org/packages/alex-tan/postgrest-client/latest/Postgrest-Client#lt) 130 | * [lte](https://package.elm-lang.org/packages/alex-tan/postgrest-client/latest/Postgrest-Client#lte) 131 | * [neq](https://package.elm-lang.org/packages/alex-tan/postgrest-client/latest/Postgrest-Client#neq) 132 | * [like](https://package.elm-lang.org/packages/alex-tan/postgrest-client/latest/Postgrest-Client#like) 133 | * [ilike](https://package.elm-lang.org/packages/alex-tan/postgrest-client/latest/Postgrest-Client#ilike) 134 | * [in](https://package.elm-lang.org/packages/alex-tan/postgrest-client/latest/Postgrest-Client#inList) 135 | * [is.null](https://package.elm-lang.org/packages/alex-tan/postgrest-client/latest/Postgrest-Client#null) 136 | * [is.true](https://package.elm-lang.org/packages/alex-tan/postgrest-client/latest/Postgrest-Client#true) 137 | * [is.false](https://package.elm-lang.org/packages/alex-tan/postgrest-client/latest/Postgrest-Client#false) 138 | * [fts](https://package.elm-lang.org/packages/alex-tan/postgrest-client/latest/Postgrest-Client#fts) 139 | * [plfts](https://package.elm-lang.org/packages/alex-tan/postgrest-client/latest/Postgrest-Client#plfts) 140 | * [phfts](https://package.elm-lang.org/packages/alex-tan/postgrest-client/latest/Postgrest-Client#plfts) 141 | * [not](https://package.elm-lang.org/packages/alex-tan/postgrest-client/latest/Postgrest-Client#not) 142 | 143 | [View Full Documentation](https://package.elm-lang.org/packages/alex-tan/postgrest-client/latest/Postgrest-Client) 144 | 145 | 146 | # URL Query Construction 147 | 148 | ## Using `select` 149 | 150 | 151 | If you're not selecting any nested resources in your request, you can use `attributes`: 152 | 153 | ```elm 154 | -- "select=id,name" 155 | P.toQueryString 156 | [ P.select <| P.attributes [ "id", "name" ] 157 | ] 158 | ``` 159 | 160 | If you want to select attributes and resources, you can use the `attribute` and `resource` functions: 161 | 162 | ```elm 163 | -- select=id,name,grades(percentage) 164 | P.toQueryString 165 | [ P.select 166 | [ P.attribute "id" 167 | , P.attribute "name" 168 | , P.resource "grades" 169 | [ P.attribute "percentage" 170 | ] 171 | ] 172 | ] 173 | ``` 174 | 175 | The library also provides a nice abstraction that allows you to both specify nested resources in a select clause, as well as use other postgrest parameters on those nested resources such as `order`, `limit`, and all of the usual conditional parameters such as `eq`: 176 | 177 | 178 | ```elm 179 | -- select=id,name,grades(percentage)&grades.order=percentage.desc&grades.limit=10 180 | P.toQueryString 181 | [ P.select 182 | [ P.attribute "id" 183 | , P.attribute "name" 184 | , P.resourceWithParams "grades" 185 | [ P.order [ P.desc "percentage" ], P.limit 10 ] 186 | [ P.attribute "percentage" 187 | ] 188 | ] 189 | ] 190 | ``` 191 | 192 | ## Conditions 193 | 194 | The library currently supports the most commonly used query parameters. Here's a sampling of how they can be used in combination with one another: 195 | 196 | ```elm 197 | -- student_id=eq.100&grade=gte.90&or=(self_evaluation.gte.90,self_evaluation.is.null) 198 | P.toQueryString 199 | [ P.param "student_id" <| P.eq <| P.int 100 200 | , P.param "grade" <| P.gte <| P.int 90 201 | , P.or 202 | [ P.param "self_evaluation" <| P.gte <| P.int 90 203 | , P.param "self_evaluation" P.null 204 | ] 205 | ] 206 | ``` 207 | 208 | The `in` operator can be used with `inList`. The second parameter is a list of whatever values you're using in your app and the first argument is the function that will transform the items in that list into the library's `Value` type such as `string` or `int`. 209 | 210 | ```elm 211 | -- name=in.("Chico","Harpo","Groucho") 212 | P.toQueryString 213 | [ P.param "name" <| P.inList P.string [ "Chico", "Harpo", "Groucho" ] 214 | ] 215 | ``` 216 | 217 | ## Order 218 | 219 | You can order results by multiple columns as well as using `nullsfirst` or `nullslast`. 220 | 221 | ```elm 222 | -- order=age.asc.nullsfirst,created_at.desc 223 | P.toQueryString 224 | [ P.order 225 | [ P.asc "age" |> P.nullsfirst 226 | , P.desc "created_at" 227 | ] 228 | ] 229 | ``` 230 | 231 | ## Combining Params 232 | 233 | Maybe you have default parameters that you want to reuse across multiple functions. You can combine them using `combineParams`: 234 | 235 | ```elm 236 | defaultParams : P.Params 237 | defaultParams = 238 | [ P.select <| P.attributes [ "id", "name" ] 239 | , P.limit 10 240 | ] 241 | 242 | 243 | constructParams : P.Params -> P.Params 244 | constructParams = 245 | P.combineParams defaultParams 246 | 247 | 248 | -- limit=100&select=id,name 249 | constructParams [ P.limit 100 ] 250 | ``` 251 | 252 | Note that the merging of the two sets is not recursive. The two are merged by the final query parameter name such as `order` or `children.order`, etc... and the second set's value is always preferred. 253 | -------------------------------------------------------------------------------- /elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "package", 3 | "name": "alex-tan/postgrest-client", 4 | "summary": "A postgrest client written in elm", 5 | "license": "MIT", 6 | "version": "2.1.1", 7 | "exposed-modules": [ 8 | "Postgrest.Client" 9 | ], 10 | "elm-version": "0.19.0 <= v < 0.20.0", 11 | "dependencies": { 12 | "elm/core": "1.0.1 <= v < 2.0.0", 13 | "elm/http": "2.0.0 <= v < 3.0.0", 14 | "elm/json": "1.1.3 <= v < 2.0.0", 15 | "elm/url": "1.0.0 <= v < 2.0.0" 16 | }, 17 | "test-dependencies": { 18 | "elm-explorations/test": "1.2.0 <= v < 2.0.0" 19 | } 20 | } -------------------------------------------------------------------------------- /examples/Examples.elm: -------------------------------------------------------------------------------- 1 | module Main exposing (main) 2 | 3 | import Browser 4 | import Html exposing (..) 5 | import Postgrest as P 6 | 7 | 8 | examples = 9 | [ ( "Simple Select", [ P.select <| P.attributes [ "id", "name" ] ] ) 10 | , ( "Select with Resources" 11 | , [ P.select 12 | [ P.attribute "id" 13 | , P.attribute "name" 14 | , P.resource "grades" 15 | [ P.attribute "percentage" 16 | ] 17 | ] 18 | ] 19 | ) 20 | , ( "Resource With Params" 21 | , [ P.select 22 | [ P.attribute "id" 23 | , P.attribute "name" 24 | , P.resourceWithParams "grades" 25 | [ P.order [ P.desc "percentage" ], P.limit 10 ] 26 | [ P.attribute "percentage" 27 | ] 28 | ] 29 | ] 30 | ) 31 | , ( "Conditions" 32 | , [ P.param "student_id" <| P.eq <| P.int 100 33 | , P.param "grade" <| P.gte <| P.int 90 34 | , P.or 35 | [ P.param "self_evaluation" <| P.gte <| P.int 90 36 | , P.param "self_evaluation" P.null 37 | ] 38 | ] 39 | ) 40 | , ( "In List" 41 | , [ P.param "name" <| P.inList P.string [ "Chico", "Harpo", "Groucho" ] 42 | ] 43 | ) 44 | , ( "Order" 45 | , [ P.order 46 | [ P.asc "age" |> P.nullsfirst 47 | , P.desc "created_at" 48 | ] 49 | ] 50 | ) 51 | , ( "Combine Params" 52 | , constructParams [ P.limit 100 ] 53 | ) 54 | ] 55 | 56 | 57 | defaultParams : P.Params 58 | defaultParams = 59 | [ P.select <| P.attributes [ "id", "name" ] 60 | , P.limit 10 61 | ] 62 | 63 | 64 | constructParams : P.Params -> P.Params 65 | constructParams = 66 | P.combineParams defaultParams 67 | 68 | 69 | type alias Model = 70 | Maybe Int 71 | 72 | 73 | type alias Msg = 74 | Maybe Int 75 | 76 | 77 | initialModel : Model 78 | initialModel = 79 | Nothing 80 | 81 | 82 | update : Msg -> Model -> Model 83 | update msg model = 84 | model 85 | 86 | 87 | view : Model -> Html Msg 88 | view model = 89 | div [] 90 | (List.map 91 | (\( desc, example ) -> 92 | div [] 93 | [ strong [] [ text <| "## " ++ desc ] 94 | , p [] [ text <| "-- " ++ P.toQueryString example ] 95 | ] 96 | ) 97 | examples 98 | ) 99 | 100 | 101 | main : Program () Model Msg 102 | main = 103 | Browser.sandbox 104 | { init = initialModel 105 | , view = view 106 | , update = update 107 | } 108 | -------------------------------------------------------------------------------- /examples/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "src" 5 | ], 6 | "elm-version": "0.19.0", 7 | "dependencies": { 8 | "direct": { 9 | "alex-tan/postgrest-client": "1.0.0", 10 | "elm/browser": "1.0.1", 11 | "elm/core": "1.0.2", 12 | "elm/html": "1.0.0" 13 | }, 14 | "indirect": { 15 | "elm/json": "1.1.3", 16 | "elm/time": "1.0.0", 17 | "elm/url": "1.0.0", 18 | "elm/virtual-dom": "1.0.2" 19 | } 20 | }, 21 | "test-dependencies": { 22 | "direct": {}, 23 | "indirect": {} 24 | } 25 | } -------------------------------------------------------------------------------- /script/watch: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | rm -rf elm-stuff; elm make; chokidar 'src/**/*.elm' -c 'elm make' -------------------------------------------------------------------------------- /src/Postgrest/Client.elm: -------------------------------------------------------------------------------- 1 | module Postgrest.Client exposing 2 | ( Endpoint 3 | , Request 4 | , endpoint 5 | , customEndpoint 6 | , getMany 7 | , getOne 8 | , postOne 9 | , getByPrimaryKey 10 | , patchByPrimaryKey 11 | , deleteByPrimaryKey 12 | , setParams 13 | , setTimeout 14 | , get 15 | , post 16 | , unsafePatch 17 | , unsafeDelete 18 | , JWT, jwt, jwtString 19 | , toCmd, toTask 20 | , PrimaryKey 21 | , primaryKey 22 | , primaryKey2 23 | , primaryKey3 24 | , Error(..), PostgrestErrorJSON, toHttpError 25 | , Param 26 | , Params 27 | , Selectable 28 | , ColumnOrder 29 | , Value 30 | , Operator 31 | , select 32 | , allAttributes 33 | , attribute 34 | , attributes 35 | , resource 36 | , resourceWithParams 37 | , combineParams 38 | , concatParams 39 | , normalizeParams 40 | , toQueryString 41 | , param 42 | , or 43 | , and 44 | , nestedParam 45 | , eq 46 | , gt 47 | , gte 48 | , inList 49 | , limit 50 | , lt 51 | , lte 52 | , neq 53 | , not 54 | , true 55 | , false 56 | , null 57 | , value 58 | , offset 59 | , ilike 60 | , like 61 | , contains 62 | , containedIn 63 | , string 64 | , int 65 | , list 66 | , order 67 | , asc 68 | , desc 69 | , nullsfirst 70 | , nullslast 71 | , plfts 72 | , phfts 73 | , fts 74 | ) 75 | 76 | {-| 77 | 78 | 79 | # postgrest-client 80 | 81 | This library allows you to construct and execute postgrest requests with additional type safety. 82 | Here's what `Api.People` might look like: 83 | 84 | import Json.Decode exposing (..) 85 | import Json.Encode as JE 86 | import Postgrest.Client as P 87 | 88 | 89 | -- Optional, but recommended to have a type that 90 | -- represents your primary key. 91 | type PersonID 92 | = PersonID Int 93 | 94 | -- And a way to unwrap it... 95 | personID : PersonID -> Int 96 | personID (PersonID id) = 97 | id 98 | 99 | -- Define the record you would fetch back from the server. 100 | type alias Person = 101 | { id : PersonID 102 | , name : String 103 | } 104 | 105 | -- Define a submission record, without the primary key. 106 | type alias PersonSubmission = 107 | { name : String 108 | } 109 | 110 | -- Decoders are written using Json.Decode 111 | decodeUnit : Decoder Person 112 | decodeUnit = 113 | map2 Person 114 | (field "id" <| map PersonID int) 115 | (field "name" string) 116 | 117 | -- Encoders are written using Json.Encode 118 | encode : PersonSubmission -> JE.Value 119 | encode person = 120 | JE.object 121 | [ ( "name", JE.string person.name ) 122 | ] 123 | 124 | -- Tell Postgrest.Client the column name of your primary key and 125 | -- how to convert it into a parameter. 126 | primaryKey : P.PrimaryKey PersonID 127 | primaryKey = 128 | P.primaryKey ( "id", P.int << personID ) 129 | 130 | -- Tell Postgrest.Client the URL of the postgrest endpoint and how 131 | -- to decode an individual record from it. Postgrest will combine 132 | -- the decoder with a list decoder automatically when necessary. 133 | endpoint : P.Endpoint Person 134 | endpoint = 135 | P.endpoint "/people" decodeUnit 136 | 137 | -- Fetch many records. If you want to specify parameters use `setParams` 138 | getMany : P.Request (List Person) 139 | getMany = 140 | P.getMany endpoint 141 | 142 | -- Delete by primary key. This is a convenience function that reduces 143 | -- the likelihood that you delete more than one record by specifying incorrect 144 | -- parameters. 145 | delete : PersonID -> P.Request PersonID 146 | delete = 147 | P.deleteByPrimaryKey endpoint primaryKey 148 | 149 | -- Create a record. 150 | post : PersonSubmission -> P.Request Person 151 | post = 152 | P.postOne endpoint << encode 153 | 154 | Here's how you could use it: 155 | 156 | import Api.People as People 157 | import Postgrest.Client as P 158 | 159 | jwt : P.JWT 160 | jwt = 161 | P.jwt "abcdefghijklmnopqrstuvwxyz1234" 162 | 163 | type Msg 164 | = PersonCreated (Result P.Error Person) 165 | | PeopleLoaded (Result P.Error (List Person)) 166 | | PersonDeleted (Result P.Error PersonID) 167 | 168 | toCmd = 169 | P.toCmd jwt 170 | 171 | cmdExamples = 172 | [ People.post 173 | { name = "Yasujirō Ozu" 174 | } 175 | |> P.toCmd jwt PersonCreated 176 | , People.getMany 177 | [ P.order [ P.asc "name" ], P.limit 10 ] 178 | |> toCmd PeopleLoaded 179 | , Person.delete personID 180 | |> toCmd PersonDeleted 181 | ] 182 | 183 | 184 | # Request Construction and Modification 185 | 186 | @docs Endpoint 187 | @docs Request 188 | @docs endpoint 189 | @docs customEndpoint 190 | 191 | 192 | # Endpoint a 193 | 194 | @docs getMany 195 | @docs getOne 196 | @docs postOne 197 | @docs getByPrimaryKey 198 | @docs patchByPrimaryKey 199 | @docs deleteByPrimaryKey 200 | 201 | 202 | # Request Options 203 | 204 | @docs setParams 205 | @docs setTimeout 206 | 207 | 208 | # Generic Requests 209 | 210 | @docs get 211 | @docs post 212 | @docs unsafePatch 213 | @docs unsafeDelete 214 | 215 | 216 | # Request Authentication 217 | 218 | @docs JWT, jwt, jwtString 219 | 220 | 221 | # Execution 222 | 223 | @docs toCmd, toTask 224 | 225 | 226 | # Primary Keys 227 | 228 | @docs PrimaryKey 229 | @docs primaryKey 230 | @docs primaryKey2 231 | @docs primaryKey3 232 | 233 | 234 | # Errors 235 | 236 | @docs Error, PostgrestErrorJSON, toHttpError 237 | 238 | 239 | # URL Parameter Construction 240 | 241 | @docs Param 242 | @docs Params 243 | @docs Selectable 244 | @docs ColumnOrder 245 | @docs Value 246 | @docs Operator 247 | 248 | 249 | ## Select 250 | 251 | @docs select 252 | @docs allAttributes 253 | @docs attribute 254 | @docs attributes 255 | @docs resource 256 | @docs resourceWithParams 257 | 258 | 259 | ## Converting/Combining Parameters 260 | 261 | @docs combineParams 262 | @docs concatParams 263 | @docs normalizeParams 264 | @docs toQueryString 265 | 266 | 267 | ## Param 268 | 269 | @docs param 270 | @docs or 271 | @docs and 272 | @docs nestedParam 273 | 274 | 275 | ## Operators 276 | 277 | @docs eq 278 | @docs gt 279 | @docs gte 280 | @docs inList 281 | @docs limit 282 | @docs lt 283 | @docs lte 284 | @docs neq 285 | @docs not 286 | @docs true 287 | @docs false 288 | @docs null 289 | @docs value 290 | @docs offset 291 | @docs ilike 292 | @docs like 293 | @docs contains 294 | @docs containedIn 295 | 296 | 297 | ## Values 298 | 299 | @docs string 300 | @docs int 301 | @docs list 302 | 303 | 304 | ## Order 305 | 306 | @docs order 307 | @docs asc 308 | @docs desc 309 | @docs nullsfirst 310 | @docs nullslast 311 | 312 | 313 | ## Full-Text Search 314 | 315 | @docs plfts 316 | @docs phfts 317 | @docs fts 318 | 319 | -} 320 | 321 | import Http exposing (Resolver, task) 322 | import Json.Decode as JD exposing (Decoder, decodeString, field, index, list, map, map4, maybe) 323 | import Json.Encode as JE 324 | import Postgrest.Internal.Endpoint as Endpoint exposing (Endpoint(..)) 325 | import Postgrest.Internal.JWT as JWT exposing (JWT) 326 | import Postgrest.Internal.Params as Param exposing (ColumnOrder(..), Language, NullOption(..), Operator(..), Param(..), Selectable(..), Value(..)) 327 | import Postgrest.Internal.Requests as Request 328 | exposing 329 | ( Request(..) 330 | , RequestType(..) 331 | , defaultRequest 332 | , fullURL 333 | , mapRequest 334 | , requestTypeToBody 335 | , requestTypeToHTTPMethod 336 | , requestTypeToHeaders 337 | , setMandatoryParams 338 | ) 339 | import Postgrest.Internal.URL exposing (BaseURL(..)) 340 | import Task exposing (Task) 341 | 342 | 343 | {-| Negate a condition. 344 | 345 | [ param "my_tsv" <| not <| phfts (Just "english") "The Fat Cats" 346 | ] 347 | |> toQueryString 348 | -- my_tsv=not.phfts(english).The%20Fat%20Cats 349 | 350 | -} 351 | not : Operator -> Operator 352 | not = 353 | Not 354 | 355 | 356 | {-| Join multiple conditions together with or. 357 | 358 | [ or 359 | [ param "age" <| gte <| int 14 360 | , param "age" <| lte <| int 18 361 | ] 362 | ] 363 | |> toQueryString 364 | 365 | -- or=(age.gte.14,age.lte.18) 366 | 367 | -} 368 | or : List Param -> Param 369 | or = 370 | Or 371 | 372 | 373 | {-| Join multiple conditions together with and. 374 | 375 | [ and 376 | [ param "grade" <| gte <| int 90 377 | , param "student" <| true 378 | , or 379 | [ param "age" <| gte <| int 14 380 | , param "age" <| null 381 | ] 382 | ] 383 | ] 384 | |> toQueryString 385 | 386 | -- and=(grade.gte.90,student.is.true,or(age.gte.14,age.is.null)) 387 | 388 | -} 389 | and : List Param -> Param 390 | and = 391 | And 392 | 393 | 394 | {-| A constructor for an individual postgrest parameter. 395 | 396 | param "name" (eq (string "John")) 397 | 398 | -} 399 | param : String -> Operator -> Param 400 | param = 401 | Param 402 | 403 | 404 | {-| Limit the number of records that can be returned. 405 | 406 | limit 10 407 | 408 | -} 409 | limit : Int -> Param 410 | limit = 411 | Limit 412 | 413 | 414 | {-| Specify the offset in the query. 415 | 416 | offset 10 417 | 418 | -} 419 | offset : Int -> Param 420 | offset = 421 | Offset 422 | 423 | 424 | {-| Normalize a string into a postgrest value. 425 | -} 426 | string : String -> Value 427 | string = 428 | String 429 | 430 | 431 | {-| Normalize an int into a postgrest value. 432 | -} 433 | int : Int -> Value 434 | int = 435 | Int 436 | 437 | 438 | {-| Sort so that nulls will come first. 439 | 440 | order [ asc "age" |> nullsfirst ] 441 | 442 | -} 443 | nullsfirst : ColumnOrder -> ColumnOrder 444 | nullsfirst o = 445 | case o of 446 | Asc s _ -> 447 | Asc s (Just NullsFirst) 448 | 449 | Desc s _ -> 450 | Desc s (Just NullsFirst) 451 | 452 | 453 | {-| Sort so that nulls will come last. 454 | 455 | order [ asc "age" |> nullslast ] 456 | 457 | -} 458 | nullslast : ColumnOrder -> ColumnOrder 459 | nullslast o = 460 | case o of 461 | Asc s _ -> 462 | Asc s (Just NullsLast) 463 | 464 | Desc s _ -> 465 | Desc s (Just NullsLast) 466 | 467 | 468 | {-| Used in combination with `order` to sort results ascending. 469 | 470 | P.order [ P.asc "name" ] 471 | 472 | -} 473 | asc : String -> ColumnOrder 474 | asc s = 475 | Asc s Nothing 476 | 477 | 478 | {-| Used in combination with `order` to sort results descending. 479 | 480 | P.order [ P.desc "name" ] 481 | 482 | -} 483 | desc : String -> ColumnOrder 484 | desc s = 485 | Desc s Nothing 486 | 487 | 488 | {-| LIKE operator (use \* in place of %) 489 | 490 | param "text" <| like "foo*bar" 491 | 492 | -} 493 | like : String -> Operator 494 | like = 495 | Like 496 | 497 | 498 | {-| ILIKE operator (use \* in place of %) 499 | 500 | param "text" <| ilike "foo*bar" 501 | 502 | -} 503 | ilike : String -> Operator 504 | ilike = 505 | Ilike 506 | 507 | 508 | {-| Query, specifying that a value should be null. 509 | 510 | param "age" <| null 511 | 512 | -} 513 | null : Operator 514 | null = 515 | Null 516 | 517 | 518 | {-| Full-Text Search using to\_tsquery 519 | 520 | [ param "my_tsv" <| fts (Just "french") "amusant" ] 521 | |> toQueryString 522 | 523 | "my_tsv=fts(french).amusant" 524 | 525 | -} 526 | fts : Maybe Language -> String -> Operator 527 | fts = 528 | Fts 529 | 530 | 531 | {-| Full-Text Search using plainto\_tsquery 532 | -} 533 | plfts : Maybe Language -> String -> Operator 534 | plfts = 535 | Plfts 536 | 537 | 538 | {-| Full-Text Search using phraseto\_tsquery 539 | -} 540 | phfts : Maybe Language -> String -> Operator 541 | phfts = 542 | Phfts 543 | 544 | 545 | {-| Used to indicate you need a column to be equal to a certain value. 546 | -} 547 | eq : Value -> Operator 548 | eq = 549 | Eq 550 | 551 | 552 | {-| Used to indicate you need a column to be not equal to a certain value. 553 | -} 554 | neq : Value -> Operator 555 | neq = 556 | Neq 557 | 558 | 559 | {-| Used to indicate you need a column to be less than a certain value. 560 | -} 561 | lt : Value -> Operator 562 | lt = 563 | Param.LT 564 | 565 | 566 | {-| Used to indicate you need a column to be greater than a certain value. 567 | -} 568 | gt : Value -> Operator 569 | gt = 570 | Param.GT 571 | 572 | 573 | {-| Used to indicate you need a column to be less than or equal than a certain value. 574 | -} 575 | lte : Value -> Operator 576 | lte = 577 | LTE 578 | 579 | 580 | {-| Used to indicate you need a column to be greater than or equal than a certain value. 581 | -} 582 | gte : Value -> Operator 583 | gte = 584 | GTE 585 | 586 | 587 | {-| Used to indicate you need a column to be within a certain list of values. 588 | 589 | param "name" <| inList string [ "Chico", "Harpo", "Groucho" ] 590 | 591 | -- name=in.(\"Chico\",\"Harpo\",\"Groucho\")" 592 | 593 | -} 594 | inList : (a -> Value) -> List a -> Operator 595 | inList toValue l = 596 | In <| List <| List.map toValue l 597 | 598 | 599 | {-| Use the `cs` operator. 600 | 601 | param "tag" <| contains <| List.map string [ "Chico", "Harpo", "Groucho" ] 602 | 603 | -- tag=cs.(\"Chico\",\"Harpo\",\"Groucho\")" 604 | 605 | -} 606 | contains : List Value -> Operator 607 | contains l = 608 | Cs l 609 | 610 | 611 | {-| Use the `cd` operator. 612 | 613 | param "tag" <| containedIn <| List.map string [ "Chico", "Harpo", "Groucho" ] 614 | 615 | -- tag=cd.(\"Chico\",\"Harpo\",\"Groucho\")" 616 | 617 | -} 618 | containedIn : List Value -> Operator 619 | containedIn l = 620 | Cd l 621 | 622 | 623 | {-| When you don't want to use a specific type after the equals sign in the query, you 624 | can use `value` to set anything you want. 625 | -} 626 | value : Value -> Operator 627 | value = 628 | Value 629 | 630 | 631 | {-| When you need a column value to be true 632 | 633 | -- foo=is.true 634 | [ P.param "foo" P.true ] 635 | |> toQueryString 636 | 637 | -} 638 | true : Operator 639 | true = 640 | Param.True 641 | 642 | 643 | {-| When you need a column value to be false 644 | 645 | -- foo=is.false 646 | [ P.param "foo" P.false ] 647 | |> toQueryString 648 | 649 | -} 650 | false : Operator 651 | false = 652 | Param.False 653 | 654 | 655 | {-| When you want to select a certain column. 656 | -} 657 | attribute : String -> Selectable 658 | attribute = 659 | Param.Attribute 660 | 661 | 662 | {-| When you want to select a nested resource with no special parameters for the nested 663 | resources. If you do want to specify parameters, see `resourceWithParams`. 664 | -} 665 | resource : String -> List Selectable -> Selectable 666 | resource name selectable = 667 | Resource name [] selectable 668 | 669 | 670 | {-| A constructor for the limit parameter. 671 | 672 | order (asc "name") 673 | 674 | order (desc "name") 675 | 676 | -} 677 | order : List ColumnOrder -> Param 678 | order = 679 | Param.order 680 | 681 | 682 | {-| A constructor for the select parameter. 683 | 684 | P.select 685 | [ P.attribute "id" 686 | , P.attribute "title" 687 | , P.resource "user" <| 688 | P.attributes 689 | [ "email" 690 | , "name" 691 | ] 692 | ] 693 | 694 | -} 695 | select : List Selectable -> Param 696 | select = 697 | Param.select 698 | 699 | 700 | {-| When you want to specify an operator for a nested resource manually. 701 | It is recommended to use resourceWithParams though. 702 | 703 | [ select 704 | [ attribute "*" 705 | , resource "actors" allAttributes 706 | ] 707 | , nestedParam [ "actors" ] <| limit 10 708 | , nestedParam [ "actors" ] <| offset 2 709 | ] 710 | |> toQueryString 711 | -- "select=*,actors(*)&actors.limit=10&actors.offset=2" 712 | 713 | -} 714 | nestedParam : List String -> Param -> Param 715 | nestedParam = 716 | Param.nestedParam 717 | 718 | 719 | {-| Takes Params and returns a query string such as 720 | `foo=eq.bar&baz=is.true` 721 | -} 722 | toQueryString : Params -> String 723 | toQueryString = 724 | Param.toQueryString 725 | 726 | 727 | {-| When you want to select a nested resource with special praameters. 728 | 729 | [ P.select 730 | [ P.resource "sites" 731 | [ P.resourceWithParams "streams" 732 | [ P.order [ P.asc "name" ] 733 | ] 734 | allAttributes 735 | ] 736 | ] 737 | ] 738 | |> toQueryString 739 | 740 | -- select=sites(streams(*))&sites.streams.order=name.asc 741 | 742 | -} 743 | resourceWithParams : String -> Params -> List Selectable -> Selectable 744 | resourceWithParams = 745 | Resource 746 | 747 | 748 | {-| This is available if you need it, but more likely you'll want to use 749 | `inList`. 750 | -} 751 | list : List Value -> Value 752 | list values = 753 | List values 754 | 755 | 756 | {-| Shorthand for attributes, when you don't need to specify nested resources: 757 | 758 | -- Short version 759 | attributes [ "id" "name" ] 760 | 761 | -- Long version 762 | [ attribute "id" 763 | , attribute "name" 764 | ] 765 | 766 | -} 767 | attributes : List String -> List Selectable 768 | attributes = 769 | List.map Attribute 770 | 771 | 772 | {-| When you want to select all attributes. This is only useful when used 773 | to select attributes of a resource or override default parameters in another function 774 | since postgrest returns all attributes by default. 775 | -} 776 | allAttributes : List Selectable 777 | allAttributes = 778 | attributes [ "*" ] 779 | 780 | 781 | {-| Used to set the parameters of your request. 782 | 783 | getPeople : P.Request (List Person) 784 | getPeople = 785 | P.getMany endpoint 786 | |> P.setParams 787 | [ P.order [ P.asc "first_name" ] 788 | , P.limit 20 789 | ] 790 | 791 | -} 792 | setParams : Params -> Request a -> Request a 793 | setParams p = 794 | mapRequest (\req -> { req | overrideParams = p }) 795 | 796 | 797 | {-| Takes Params and returns the parameters as a list of (Key, Value) strings. 798 | -} 799 | normalizeParams : Params -> List ( String, String ) 800 | normalizeParams = 801 | Param.normalizeParams 802 | 803 | 804 | {-| Takes a list of Params and combines them, preferring the last sets first. 805 | -} 806 | concatParams : List Params -> Params 807 | concatParams = 808 | Param.concatParams 809 | 810 | 811 | {-| Takes a default set of params and a custom set of params and prefers the second set. 812 | Useful when you're constructing reusable functions that make similar queries. 813 | -} 814 | combineParams : Params -> Params -> Params 815 | combineParams = 816 | Param.combineParams 817 | 818 | 819 | {-| A type that represents the operator of a query. In `name=eq.John` the operator would be the `=`. 820 | -} 821 | type alias Operator = 822 | Param.Operator 823 | 824 | 825 | {-| Type that can be represented in the queries: strings, ints and lists. 826 | -} 827 | type alias Value = 828 | Param.Value 829 | 830 | 831 | {-| A type to specify whether you want an order to be ascending or descending, and 832 | optionally whether you want nulls to be first or last. 833 | -} 834 | type alias ColumnOrder = 835 | Param.ColumnOrder 836 | 837 | 838 | {-| A type representing which attributes and resources you want to select. 839 | It also contains parameters that target nested resources. 840 | -} 841 | type alias Selectable = 842 | Param.Selectable 843 | 844 | 845 | {-| A list of Param. 846 | -} 847 | type alias Params = 848 | List Param 849 | 850 | 851 | {-| An individual postgrest parameter. 852 | -} 853 | type alias Param = 854 | Param.Param 855 | 856 | 857 | {-| Used to GET multiple records from the provided endpoint. 858 | Converts your endpoint decoder into `(list decoder)` to decode multiple records. 859 | 860 | endpoint : P.Endpoint Person 861 | endpoint = 862 | P.endpoint "/people" decodePerson 863 | 864 | getAll : P.Request (List Person) 865 | getAll = 866 | P.getMany endpoint 867 | |> P.setParams [ P.limit 20 ] 868 | 869 | -} 870 | getMany : Endpoint a -> Request (List a) 871 | getMany e = 872 | defaultRequest e <| Get <| JD.list <| Endpoint.decoder e 873 | 874 | 875 | {-| Used to GET a single record. Converts your endpoint decoder into `(index 0 decoder)` to extract 876 | it from postgrest's JSON array response and sets `limit=1` in the parameters. If you're requesting by 877 | primary key see `getOneByPrimaryKey`. 878 | 879 | endpoint : P.Endpoint Person 880 | endpoint = 881 | P.endpoint "/people" decodePerson 882 | 883 | getOnePersonByName : String -> P.Request Person 884 | getOnePersonByName name = 885 | P.getOne endpoint 886 | |> P.setParams [ P.param "name" <| P.eq name ] 887 | 888 | -} 889 | getOne : Endpoint a -> Request a 890 | getOne e = 891 | (defaultRequest e <| Get <| JD.index 0 <| Endpoint.decoder e) 892 | |> setParams [ limit 1 ] 893 | 894 | 895 | {-| The most basic way to make a get request. 896 | -} 897 | get : 898 | String 899 | -> 900 | { params : Params 901 | , decoder : Decoder a 902 | } 903 | -> Request a 904 | get baseURL { params, decoder } = 905 | Request 906 | { options = Get decoder 907 | , timeout = Nothing 908 | , defaultParams = [] 909 | , overrideParams = params 910 | , mandatoryParams = [] 911 | , baseURL = BaseURL baseURL 912 | } 913 | 914 | 915 | {-| The most basic way to make a post request. 916 | -} 917 | post : 918 | String 919 | -> 920 | { params : Params 921 | , decoder : Decoder a 922 | , body : JE.Value 923 | } 924 | -> Request a 925 | post baseURL { params, decoder, body } = 926 | Request 927 | { options = Post body decoder 928 | , timeout = Nothing 929 | , defaultParams = [] 930 | , overrideParams = params 931 | , mandatoryParams = [] 932 | , baseURL = BaseURL baseURL 933 | } 934 | 935 | 936 | {-| Titled unsafe because if you provide incorrect or no parameters it will make a PATCH request 937 | to all resources the requesting user has access to at that endpoint. Use with caution. 938 | See [Block Full-Table Operations](http://postgrest.org/en/v5.2/admin.html#block-fulltable). 939 | -} 940 | unsafePatch : 941 | String 942 | -> 943 | { body : JE.Value 944 | , decoder : Decoder a 945 | , params : Params 946 | } 947 | -> Request a 948 | unsafePatch baseURL { body, decoder, params } = 949 | Request 950 | { options = Patch body decoder 951 | , timeout = Nothing 952 | , defaultParams = [] 953 | , overrideParams = params 954 | , mandatoryParams = [] 955 | , baseURL = BaseURL baseURL 956 | } 957 | 958 | 959 | type alias UnsafeDeleteOptions a = 960 | { params : Params 961 | , returning : a 962 | } 963 | 964 | 965 | {-| Titled unsafe because if you provide incorrect or no parameters it will make a DELETE request 966 | to all resources the requesting user has access to at that endpoint. Use with caution. 967 | See [Block Full-Table Operations](http://postgrest.org/en/v5.2/admin.html#block-fulltable). 968 | -} 969 | unsafeDelete : String -> UnsafeDeleteOptions a -> Request a 970 | unsafeDelete url { returning, params } = 971 | Request 972 | { options = Delete returning 973 | , timeout = Nothing 974 | , defaultParams = [] 975 | , overrideParams = params 976 | , mandatoryParams = [] 977 | , baseURL = BaseURL url 978 | } 979 | 980 | 981 | primaryKeyEqClause : PrimaryKey primaryKey -> primaryKey -> Params 982 | primaryKeyEqClause converter pk = 983 | let 984 | pkPartToParam ( key, toParam ) = 985 | param key <| eq <| toParam pk 986 | 987 | targetCondition = 988 | case converter of 989 | PrimaryKey [ a ] -> 990 | pkPartToParam a 991 | 992 | PrimaryKey xs -> 993 | xs 994 | |> List.map pkPartToParam 995 | |> and 996 | in 997 | [ targetCondition 998 | , limit 1 999 | ] 1000 | 1001 | 1002 | {-| Used to GET a single record by primary key. This is the recommended way to do a singular GET request 1003 | assuming your table has a primary key. 1004 | 1005 | endpoint : P.Endpoint Person 1006 | endpoint = 1007 | P.endpoint "/people" decodePerson 1008 | 1009 | primaryKey : P.PrimaryKey Int 1010 | primaryKey = 1011 | P.primaryKey ( "id", P.int ) 1012 | 1013 | getByPrimaryKey : Int -> P.Request Person 1014 | getByPrimaryKey = 1015 | P.getByPrimaryKey endpoint primaryKey 1016 | 1017 | -} 1018 | getByPrimaryKey : Endpoint a -> PrimaryKey p -> p -> Request a 1019 | getByPrimaryKey e primaryKeyToParams_ primaryKey_ = 1020 | defaultRequest e (Get <| index 0 <| Endpoint.decoder e) 1021 | |> setMandatoryParams (primaryKeyEqClause primaryKeyToParams_ primaryKey_) 1022 | 1023 | 1024 | {-| Used to PATCH a single record by primary key. This is the recommended way to do a PATCH request 1025 | assuming your table has a primary key. The decoder will decode the record after it's been patched if the request is successful. 1026 | 1027 | endpoint : P.Endpoint Person 1028 | endpoint = 1029 | P.endpoint "/people" decodePerson 1030 | 1031 | primaryKey = 1032 | P.primaryKey ( "id", P.int ) 1033 | 1034 | updatePerson : PersonForm -> Int -> P.Request Person 1035 | updatePerson submission id = 1036 | P.patchByPrimaryKey endpoint primaryKey (encodeSubmission submission) 1037 | 1038 | -- Would create a request to patch to "/people?id=eq.3" 1039 | updatePerson form 3 1040 | 1041 | -} 1042 | patchByPrimaryKey : Endpoint a -> PrimaryKey p -> p -> JE.Value -> Request a 1043 | patchByPrimaryKey e primaryKeyToParams primaryKey_ body = 1044 | defaultRequest e (Patch body <| index 0 <| Endpoint.decoder e) 1045 | |> setMandatoryParams (primaryKeyEqClause primaryKeyToParams primaryKey_) 1046 | 1047 | 1048 | {-| Used to DELETE a single record by primary key. This is the recommended way to do a DELETE request 1049 | if your table has a primary key. The decoder will decode the record after it's been patched if the request is successful. 1050 | 1051 | endpoint : P.Endpoint Person 1052 | endpoint = 1053 | P.endpoint "/people" decodePerson 1054 | 1055 | primaryKey = 1056 | P.primaryKey ( "id", P.int ) 1057 | 1058 | delete : Int -> P.Request Int 1059 | delete = 1060 | P.deleteByPrimaryKey endpoint primaryKey 1061 | 1062 | -- Would create a request to DELETE to "/people?id=eq.3" 1063 | -- and the success value would be the ID passed in. 1064 | -- So your Msg would look like: 1065 | -- | DeleteSuccess (Result P.Error Int) 1066 | delete 3 1067 | 1068 | -} 1069 | deleteByPrimaryKey : Endpoint a -> PrimaryKey p -> p -> Request p 1070 | deleteByPrimaryKey e primaryKeyToParams primaryKey_ = 1071 | defaultRequest e (Delete primaryKey_) 1072 | |> setMandatoryParams (primaryKeyEqClause primaryKeyToParams primaryKey_) 1073 | 1074 | 1075 | {-| Used to create a single record at the endpoint you provide and an encoded JSON value. 1076 | 1077 | endpoint : P.Endpoint Person 1078 | endpoint = 1079 | P.endpoint "/people" decodePerson 1080 | 1081 | encodePerson : Person -> JE.Value 1082 | encodePerson p = 1083 | object 1084 | [ ( "first_name", JE.string p.firstName ) 1085 | , ( "last_name", JE.string p.lastName ) 1086 | ] 1087 | 1088 | post : PersonForm -> P.Request Person 1089 | post submission = 1090 | P.postOne endpoint (encodePerson submission) 1091 | 1092 | -} 1093 | postOne : Endpoint a -> JE.Value -> Request a 1094 | postOne e body = 1095 | defaultRequest e <| Post body <| index 0 <| Endpoint.decoder e 1096 | 1097 | 1098 | {-| Takes a JWT, Msg and a Request and turns it into a Cmd. 1099 | -} 1100 | toCmd : JWT -> (Result Error a -> msg) -> Request a -> Cmd msg 1101 | toCmd jwt_ toMsg (Request options) = 1102 | Http.request 1103 | { method = requestTypeToHTTPMethod options.options 1104 | , headers = requestTypeToHeaders jwt_ options.options 1105 | , url = fullURL options 1106 | , body = requestTypeToBody options.options 1107 | , timeout = options.timeout 1108 | , tracker = Nothing 1109 | , expect = 1110 | case options.options of 1111 | Delete returning -> 1112 | expectWhatever (toMsg << Result.map (always returning)) 1113 | 1114 | Get decoder -> 1115 | expectJson toMsg decoder 1116 | 1117 | Post _ decoder -> 1118 | expectJson toMsg decoder 1119 | 1120 | Patch _ decoder -> 1121 | expectJson toMsg decoder 1122 | } 1123 | 1124 | 1125 | {-| Takes a JWT and a Request and turns it into a Task. 1126 | -} 1127 | toTask : JWT -> Request a -> Task Error a 1128 | toTask jwt_ (Request o) = 1129 | let 1130 | { options } = 1131 | o 1132 | in 1133 | task 1134 | { body = requestTypeToBody options 1135 | , timeout = o.timeout 1136 | , url = fullURL o 1137 | , method = requestTypeToHTTPMethod options 1138 | , headers = requestTypeToHeaders jwt_ options 1139 | , resolver = 1140 | case options of 1141 | Delete returning -> 1142 | Http.stringResolver <| always <| Ok returning 1143 | 1144 | Get decoder -> 1145 | jsonResolver decoder 1146 | 1147 | Post _ decoder -> 1148 | jsonResolver decoder 1149 | 1150 | Patch _ decoder -> 1151 | jsonResolver decoder 1152 | } 1153 | 1154 | 1155 | jsonResolver : Decoder a -> Resolver Error a 1156 | jsonResolver = 1157 | Http.stringResolver << resolution 1158 | 1159 | 1160 | resolution : Decoder a -> Http.Response String -> Result Error a 1161 | resolution decoder response = 1162 | case response of 1163 | Http.BadUrl_ url_ -> 1164 | Err <| BadUrl url_ 1165 | 1166 | Http.Timeout_ -> 1167 | Err Timeout 1168 | 1169 | Http.NetworkError_ -> 1170 | Err NetworkError 1171 | 1172 | Http.BadStatus_ metadata body -> 1173 | Err <| badStatusBodyToPostgrestError metadata.statusCode body 1174 | 1175 | Http.GoodStatus_ _ body -> 1176 | case JD.decodeString decoder body of 1177 | Ok value_ -> 1178 | Ok value_ 1179 | 1180 | Err err -> 1181 | Err <| BadBody <| JD.errorToString err 1182 | 1183 | 1184 | expectJson : (Result Error a -> msg) -> Decoder a -> Http.Expect msg 1185 | expectJson toMsg decoder = 1186 | Http.expectStringResponse toMsg (resolution decoder) 1187 | 1188 | 1189 | expectWhatever : (Result Error () -> msg) -> Http.Expect msg 1190 | expectWhatever toMsg = 1191 | let 1192 | resolve : (body -> Result String a) -> Http.Response body -> Result Error a 1193 | resolve toResult response = 1194 | case response of 1195 | Http.BadUrl_ url_ -> 1196 | Err <| BadUrl url_ 1197 | 1198 | Http.Timeout_ -> 1199 | Err Timeout 1200 | 1201 | Http.NetworkError_ -> 1202 | Err NetworkError 1203 | 1204 | Http.BadStatus_ metadata _ -> 1205 | Err <| BadStatus metadata.statusCode "" emptyErrors 1206 | 1207 | Http.GoodStatus_ _ body -> 1208 | Result.mapError BadBody <| toResult body 1209 | in 1210 | Http.expectStringResponse toMsg <| resolve <| always <| Ok () 1211 | 1212 | 1213 | {-| Can be used together with endpoint to make request construction easier. See 1214 | [primaryKey](#primaryKey) and [endpoint](#endpoint). 1215 | -} 1216 | type PrimaryKey pk 1217 | = PrimaryKey (List ( String, pk -> Value )) 1218 | 1219 | 1220 | {-| Used to construct a primary key made up of one column. 1221 | Takes a tuple of the column name of your primary key and a function 1222 | to convert your elm representation of that primary key into a postgrest parameter. 1223 | 1224 | primaryKey : P.PrimaryKey Int 1225 | primaryKey = 1226 | primaryKey ( "id", P.int ) 1227 | 1228 | is the simplest example. If you have custom type to represent your primary key you 1229 | could do this: 1230 | 1231 | type ID 1232 | = ID Int 1233 | 1234 | idToInt : ID -> Int 1235 | idToInt (ID id) = 1236 | id 1237 | 1238 | primaryKey : P.PrimaryKey ID 1239 | primaryKey = 1240 | P.primaryKey ( "id", P.int << idToInt ) 1241 | 1242 | -} 1243 | primaryKey : ( String, pk -> Value ) -> PrimaryKey pk 1244 | primaryKey a = 1245 | PrimaryKey [ a ] 1246 | 1247 | 1248 | {-| Used to construct a primary key made up of two columns. 1249 | Takes two tuples, each with a column name and a function 1250 | to convert your elm representation of that primary key into a postgrest parameter. 1251 | 1252 | primaryKey2 ( "id", P.int ) 1253 | 1254 | is the simplest example. If you have custom type to represent your primary key you 1255 | could do this: 1256 | 1257 | type alias ParentID = 1258 | Int 1259 | 1260 | type alias Category = 1261 | String 1262 | 1263 | type alias MyPrimaryKey = 1264 | ( ParentID, Category ) 1265 | 1266 | primaryKey : P.PrimaryKey MyPrimaryKey 1267 | primaryKey = 1268 | P.primaryKey2 1269 | ( "parent_id", P.int << Tuple.first ) 1270 | ( "category", P.string << Tuple.second ) 1271 | 1272 | -} 1273 | primaryKey2 : ( String, pk -> Value ) -> ( String, pk -> Value ) -> PrimaryKey pk 1274 | primaryKey2 a b = 1275 | PrimaryKey [ a, b ] 1276 | 1277 | 1278 | {-| Used to construct primary keys that are made up of three columns. See [primaryKey2](#primaryKey2) for 1279 | a similar example of how this could be used. 1280 | -} 1281 | primaryKey3 : ( String, pk -> Value ) -> ( String, pk -> Value ) -> ( String, pk -> Value ) -> PrimaryKey pk 1282 | primaryKey3 a b c = 1283 | PrimaryKey [ a, b, c ] 1284 | 1285 | 1286 | {-| The simplest way to define an endpoint. You provide it the URL and a decoder. 1287 | It can then be used to quickly construct POST, GET, PATCH, and DELETE requests. 1288 | The decoder provided should just be a decoder of the record itself, not a decoder of 1289 | an object inside an array. 1290 | 1291 | decodePerson : Decoder Person 1292 | decodePerson = 1293 | map2 Person 1294 | (field "first_name" string) 1295 | (field "last_name" string) 1296 | 1297 | peopleEndpoint : P.Endpoint Person 1298 | peopleEndpoint = 1299 | P.endpoint "/rest/people" decodePerson 1300 | 1301 | -} 1302 | endpoint : String -> Decoder a -> Endpoint a 1303 | endpoint a decoder = 1304 | Endpoint 1305 | { url = BaseURL a 1306 | , decoder = decoder 1307 | , defaultSelect = Nothing 1308 | , defaultOrder = Nothing 1309 | } 1310 | 1311 | 1312 | {-| Define an endpoint with extra options. To quickly construct POST, GET, PATCH, and DELETE requests. 1313 | `defaultOrder` and `defaultSelect` can be overriden by using `setParams` once a request is constructed. 1314 | 1315 | peopleEndpoint : P.Endpoint Person 1316 | peopleEndpoint = 1317 | P.endpoint "/rest/people" 1318 | decodePerson 1319 | { defaultSelect = Just [ P.attribute "id", P.attribute "name" ] 1320 | , defaultOrder = Just [ P.asc "name" ] 1321 | } 1322 | 1323 | -} 1324 | customEndpoint : 1325 | String 1326 | -> Decoder a 1327 | -> 1328 | { defaultSelect : Maybe (List Selectable) 1329 | , defaultOrder : Maybe (List ColumnOrder) 1330 | } 1331 | -> Endpoint a 1332 | customEndpoint u decoder { defaultSelect, defaultOrder } = 1333 | Endpoint 1334 | { url = BaseURL u 1335 | , decoder = decoder 1336 | , defaultSelect = defaultSelect 1337 | , defaultOrder = defaultOrder 1338 | } 1339 | 1340 | 1341 | {-| Contains any details postgrest might have given us about a failed request. 1342 | -} 1343 | type alias PostgrestErrorJSON = 1344 | { message : Maybe String 1345 | , details : Maybe String 1346 | , hint : Maybe String 1347 | , code : Maybe String 1348 | } 1349 | 1350 | 1351 | decodePostgrestError : Decoder PostgrestErrorJSON 1352 | decodePostgrestError = 1353 | map4 PostgrestErrorJSON 1354 | (maybe (field "message" JD.string)) 1355 | (maybe (field "details" JD.string)) 1356 | (maybe (field "hint" JD.string)) 1357 | (maybe (field "code" JD.string)) 1358 | 1359 | 1360 | emptyErrors : PostgrestErrorJSON 1361 | emptyErrors = 1362 | PostgrestErrorJSON 1363 | Nothing 1364 | Nothing 1365 | Nothing 1366 | Nothing 1367 | 1368 | 1369 | {-| `Error` Looks a lot like `Http.Error` except `BadStatus` includes a second argument, 1370 | `PostgrestErrorJSON` with any details that postgrest might have given us about a failed request. 1371 | -} 1372 | type Error 1373 | = Timeout 1374 | | BadUrl String 1375 | | NetworkError 1376 | | BadStatus Int String PostgrestErrorJSON 1377 | | BadBody String 1378 | 1379 | 1380 | {-| Converts the custom HTTP error used by this package into an elm/http Error. 1381 | This can be useful if you're using `Task.map2`, `Task.map3`, etc... and each of the 1382 | tasks need to have the same error type. 1383 | -} 1384 | toHttpError : Error -> Http.Error 1385 | toHttpError e = 1386 | case e of 1387 | Timeout -> 1388 | Http.Timeout 1389 | 1390 | BadUrl s -> 1391 | Http.BadUrl s 1392 | 1393 | NetworkError -> 1394 | Http.NetworkError 1395 | 1396 | BadStatus i _ _ -> 1397 | Http.BadStatus i 1398 | 1399 | BadBody s -> 1400 | Http.BadBody s 1401 | 1402 | 1403 | badStatusBodyToPostgrestError : Int -> String -> Error 1404 | badStatusBodyToPostgrestError statusCode body = 1405 | BadStatus statusCode body <| bodyToPostgrestErrors body 1406 | 1407 | 1408 | bodyToPostgrestErrors : String -> PostgrestErrorJSON 1409 | bodyToPostgrestErrors body = 1410 | case JD.decodeString decodePostgrestError body of 1411 | Ok errors -> 1412 | errors 1413 | 1414 | Err _ -> 1415 | emptyErrors 1416 | 1417 | 1418 | {-| If you've already created a JWT with `jwt` you can extract the original string with 1419 | this function. 1420 | 1421 | myJWT = P.jwt "abcdef" 1422 | 1423 | jwtString myJWT -- "abcdef" 1424 | 1425 | -} 1426 | jwtString : JWT -> String 1427 | jwtString = 1428 | JWT.jwtString 1429 | 1430 | 1431 | {-| Pass the jwt string into this function to make it a JWT. This is used with `toCmd` and `toTask` 1432 | to make requests. 1433 | 1434 | myJWT = 1435 | P.jwt "abcdef" 1436 | 1437 | -} 1438 | jwt : String -> JWT 1439 | jwt = 1440 | JWT.jwt 1441 | 1442 | 1443 | {-| The type used to store the JWT string. 1444 | -} 1445 | type alias JWT = 1446 | JWT.JWT 1447 | 1448 | 1449 | {-| Request can be used with toCmd and toTask to make a request. 1450 | -} 1451 | type alias Request r = 1452 | Request.Request r 1453 | 1454 | 1455 | {-| Sets the timeout of your request. The behaviour is the same 1456 | of that in the elm/http package. 1457 | -} 1458 | setTimeout : Float -> Request a -> Request a 1459 | setTimeout = 1460 | Request.setTimeout 1461 | 1462 | 1463 | {-| Think of an Endpoint as a combination between a base url like `/schools` and 1464 | an elm/json `Decoder`. The endpoint can be passed to other functions in this library, 1465 | sometimes along with PrimaryKey to make constructing certain types of requests easier. 1466 | -} 1467 | type alias Endpoint a = 1468 | Endpoint.Endpoint a 1469 | -------------------------------------------------------------------------------- /src/Postgrest/Internal/Endpoint.elm: -------------------------------------------------------------------------------- 1 | module Postgrest.Internal.Endpoint exposing 2 | ( Endpoint(..) 3 | , EndpointOptions 4 | , decoder 5 | , defaultParams 6 | , url 7 | ) 8 | 9 | import Json.Decode exposing (Decoder) 10 | import Postgrest.Internal.Params exposing (ColumnOrder, Params, Selectable, order, select) 11 | import Postgrest.Internal.URL exposing (BaseURL) 12 | 13 | 14 | type Endpoint a 15 | = Endpoint (EndpointOptions a) 16 | 17 | 18 | type alias EndpointOptions a = 19 | { url : BaseURL 20 | , decoder : Decoder a 21 | , defaultSelect : Maybe (List Selectable) 22 | , defaultOrder : Maybe (List ColumnOrder) 23 | } 24 | 25 | 26 | defaultParams : Endpoint a -> Params 27 | defaultParams (Endpoint { defaultSelect, defaultOrder }) = 28 | [ defaultSelect |> Maybe.map select 29 | , defaultOrder |> Maybe.map order 30 | ] 31 | |> List.filterMap identity 32 | 33 | 34 | decoder : Endpoint a -> Decoder a 35 | decoder (Endpoint o) = 36 | o.decoder 37 | 38 | 39 | url : Endpoint a -> BaseURL 40 | url (Endpoint o) = 41 | o.url 42 | -------------------------------------------------------------------------------- /src/Postgrest/Internal/JWT.elm: -------------------------------------------------------------------------------- 1 | module Postgrest.Internal.JWT exposing (JWT(..), jwt, jwtHeader, jwtString) 2 | 3 | import Http 4 | 5 | 6 | type JWT 7 | = JWT String 8 | 9 | 10 | jwt : String -> JWT 11 | jwt = 12 | JWT 13 | 14 | 15 | jwtString : JWT -> String 16 | jwtString (JWT s) = 17 | s 18 | 19 | 20 | jwtHeader : JWT -> Http.Header 21 | jwtHeader (JWT jwt_) = 22 | Http.header "Authorization" <| "Bearer " ++ jwt_ 23 | -------------------------------------------------------------------------------- /src/Postgrest/Internal/Params.elm: -------------------------------------------------------------------------------- 1 | module Postgrest.Internal.Params exposing 2 | ( ColumnOrder(..) 3 | , Language 4 | , NullOption(..) 5 | , Operator(..) 6 | , Param(..) 7 | , Params 8 | , Selectable(..) 9 | , Value(..) 10 | , combineParams 11 | , concatParams 12 | , nestedParam 13 | , normalizeParams 14 | , order 15 | , select 16 | , toQueryString 17 | ) 18 | 19 | import Dict exposing (Dict) 20 | import Url 21 | 22 | 23 | type alias Params = 24 | List Param 25 | 26 | 27 | type Param 28 | = Param String Operator 29 | | NestedParam String Param 30 | | Select (List Selectable) 31 | | Limit Int 32 | | Offset Int 33 | | Order (List ColumnOrder) 34 | | Or (List Param) 35 | | And (List Param) 36 | 37 | 38 | type ColumnOrder 39 | = Asc String (Maybe NullOption) 40 | | Desc String (Maybe NullOption) 41 | 42 | 43 | type NullOption 44 | = NullsFirst 45 | | NullsLast 46 | 47 | 48 | type Operator 49 | = Eq Value 50 | | GT Value 51 | | GTE Value 52 | | LT Value 53 | | LTE Value 54 | | Neq Value 55 | | Like String 56 | | Ilike String 57 | | In Value 58 | | Null 59 | | True 60 | | False 61 | | Fts (Maybe Language) String 62 | | Plfts (Maybe Language) String 63 | | Phfts (Maybe Language) String 64 | | Cs (List Value) 65 | | Cd (List Value) 66 | -- | Ov Range 67 | -- | Sl Range 68 | -- | Sr Range 69 | -- | Nxr Range 70 | -- | Nxl Range 71 | -- | Adj Range 72 | | Not Operator 73 | | Value Value 74 | 75 | 76 | type Selectable 77 | = Attribute String 78 | | Resource String (List Param) (List Selectable) 79 | 80 | 81 | type Value 82 | = String String 83 | | Int Int 84 | | List (List Value) 85 | 86 | 87 | type alias Language = 88 | String 89 | 90 | 91 | order : List ColumnOrder -> Param 92 | order = 93 | Order 94 | 95 | 96 | select : List Selectable -> Param 97 | select = 98 | Select 99 | 100 | 101 | concatParams : List Params -> Params 102 | concatParams = 103 | List.foldl 104 | (\a acc -> 105 | combineParams acc a 106 | ) 107 | [] 108 | 109 | 110 | combineParams : Params -> Params -> Params 111 | combineParams defaults override = 112 | Dict.union 113 | (dictifyParams override) 114 | (dictifyParams defaults) 115 | |> Dict.values 116 | 117 | 118 | curlyBraceList : List Value -> String 119 | curlyBraceList ls = 120 | let 121 | inner = 122 | ls 123 | |> List.map stringifyUnquoted 124 | |> String.join "," 125 | in 126 | "{" ++ inner ++ "}" 127 | 128 | 129 | dictifyParams : Params -> Dict String Param 130 | dictifyParams = 131 | List.map (\p -> ( postgrestParamKey p, p )) >> Dict.fromList 132 | 133 | 134 | postgrestParamKey : Param -> String 135 | postgrestParamKey p = 136 | case p of 137 | Limit _ -> 138 | "limit" 139 | 140 | Offset _ -> 141 | "offset" 142 | 143 | Param k _ -> 144 | k 145 | 146 | Select _ -> 147 | "select" 148 | 149 | Order _ -> 150 | "order" 151 | 152 | Or _ -> 153 | "or" 154 | 155 | And _ -> 156 | "and" 157 | 158 | NestedParam r param_ -> 159 | r ++ "." ++ postgrestParamKey param_ 160 | 161 | 162 | postgrestParamValue : Param -> String 163 | postgrestParamValue p = 164 | case p of 165 | Param _ clause -> 166 | stringifyClause clause 167 | 168 | Select attrs -> 169 | attrs 170 | |> List.map stringifySelect 171 | |> String.join "," 172 | 173 | Limit i -> 174 | String.fromInt i 175 | 176 | Offset i -> 177 | String.fromInt i 178 | 179 | Order os -> 180 | os 181 | |> List.map 182 | (\o -> 183 | case o of 184 | Asc field nullOption -> 185 | [ Just field 186 | , Just "asc" 187 | , stringifyNullOption nullOption 188 | ] 189 | |> catMaybes 190 | |> String.join "." 191 | 192 | Desc field nullOption -> 193 | [ Just field 194 | , Just "desc" 195 | , stringifyNullOption nullOption 196 | ] 197 | |> catMaybes 198 | |> String.join "." 199 | ) 200 | |> String.join "," 201 | 202 | And c -> 203 | wrapConditions c 204 | 205 | Or c -> 206 | wrapConditions c 207 | 208 | NestedParam _ nestedParam_ -> 209 | postgrestParamValue nestedParam_ 210 | 211 | 212 | stringifyNullOption : Maybe NullOption -> Maybe String 213 | stringifyNullOption = 214 | Maybe.map 215 | (\n_ -> 216 | case n_ of 217 | NullsFirst -> 218 | "nullsfirst" 219 | 220 | NullsLast -> 221 | "nullslast" 222 | ) 223 | 224 | 225 | wrapConditions : Params -> String 226 | wrapConditions = 227 | List.concatMap normalizeParam 228 | >> List.map paramToInnerString 229 | >> String.join "," 230 | >> surroundInParens 231 | 232 | 233 | surroundInParens : String -> String 234 | surroundInParens s = 235 | "(" ++ s ++ ")" 236 | 237 | 238 | stringifyClause : Operator -> String 239 | stringifyClause operator = 240 | case operator of 241 | Neq val -> 242 | "neq." ++ stringifyUnquoted val 243 | 244 | Eq val -> 245 | "eq." ++ stringifyUnquoted val 246 | 247 | In val -> 248 | "in.(" ++ stringifyQuoted val ++ ")" 249 | 250 | Cs val -> 251 | "cs." ++ curlyBraceList val 252 | 253 | Cd val -> 254 | "cd." ++ curlyBraceList val 255 | 256 | Value val -> 257 | stringifyUnquoted val 258 | 259 | True -> 260 | "is.true" 261 | 262 | False -> 263 | "is.false" 264 | 265 | Null -> 266 | "is.null" 267 | 268 | LT val -> 269 | "lt." ++ stringifyQuoted val 270 | 271 | LTE val -> 272 | "lte." ++ stringifyQuoted val 273 | 274 | GT val -> 275 | "gt." ++ stringifyQuoted val 276 | 277 | GTE val -> 278 | "gte." ++ stringifyQuoted val 279 | 280 | Not o -> 281 | "not." ++ stringifyClause o 282 | 283 | Fts lang val -> 284 | fullTextSearch "fts" lang val 285 | 286 | Like s -> 287 | "like." ++ (stringifyUnquoted <| String s) 288 | 289 | Ilike s -> 290 | "ilike." ++ (stringifyUnquoted <| String s) 291 | 292 | Plfts lang val -> 293 | fullTextSearch "plfts" lang val 294 | 295 | Phfts lang val -> 296 | fullTextSearch "phfts" lang val 297 | 298 | 299 | catMaybes : List (Maybe a) -> List a 300 | catMaybes = 301 | List.filterMap identity 302 | 303 | 304 | fullTextSearch : String -> Maybe String -> String -> String 305 | fullTextSearch operator lang val = 306 | operator 307 | ++ (lang 308 | |> Maybe.map surroundInParens 309 | |> Maybe.withDefault "" 310 | ) 311 | ++ "." 312 | ++ stringifyValue Basics.False (String val) 313 | 314 | 315 | stringifyUnquoted : Value -> String 316 | stringifyUnquoted = 317 | stringifyValue Basics.False 318 | 319 | 320 | stringifyQuoted : Value -> String 321 | stringifyQuoted = 322 | stringifyValue Basics.True 323 | 324 | 325 | stringifyValue : Bool -> Value -> String 326 | stringifyValue quotes val = 327 | case val of 328 | String str -> 329 | if quotes then 330 | "\"" ++ Url.percentEncode str ++ "\"" 331 | 332 | else 333 | Url.percentEncode str 334 | 335 | Int i -> 336 | String.fromInt i 337 | 338 | List l -> 339 | l 340 | |> List.map (stringifyValue quotes) 341 | |> String.join "," 342 | 343 | 344 | stringifySelect : Selectable -> String 345 | stringifySelect postgrestSelect = 346 | case postgrestSelect of 347 | Attribute attr -> 348 | attr 349 | 350 | Resource resourceName _ attrs -> 351 | case attrs of 352 | [] -> 353 | resourceName 354 | 355 | _ -> 356 | resourceName 357 | ++ "(" 358 | ++ (attrs 359 | |> List.map stringifySelect 360 | |> String.join "," 361 | ) 362 | ++ ")" 363 | 364 | 365 | normalizeParams : Params -> List ( String, String ) 366 | normalizeParams = 367 | List.concatMap normalizeParam 368 | 369 | 370 | normalizeParam : Param -> List ( String, String ) 371 | normalizeParam p = 372 | case p of 373 | Select selection -> 374 | ( postgrestParamKey p, postgrestParamValue p ) :: selectionParams selection 375 | 376 | _ -> 377 | [ ( postgrestParamKey p, postgrestParamValue p ) ] 378 | 379 | 380 | selectionParams : List Selectable -> List ( String, String ) 381 | selectionParams = 382 | List.concatMap (selectionParam []) 383 | 384 | 385 | selectionParam : List String -> Selectable -> List ( String, String ) 386 | selectionParam context s = 387 | case s of 388 | Attribute _ -> 389 | [] 390 | 391 | Resource name options_ nested -> 392 | let 393 | newContext = 394 | context ++ [ name ] 395 | in 396 | List.map 397 | (\item -> 398 | let 399 | p = 400 | nestedParam newContext item 401 | in 402 | ( postgrestParamKey p, postgrestParamValue p ) 403 | ) 404 | options_ 405 | ++ List.concatMap (selectionParam newContext) nested 406 | 407 | 408 | paramToString : ( String, String ) -> String 409 | paramToString ( k, v ) = 410 | k ++ "=" ++ v 411 | 412 | 413 | paramToInnerString : ( String, String ) -> String 414 | paramToInnerString ( k, v ) = 415 | case k of 416 | "and" -> 417 | k ++ v 418 | 419 | "or" -> 420 | k ++ v 421 | 422 | _ -> 423 | k ++ "." ++ v 424 | 425 | 426 | nestedParam : List String -> Param -> Param 427 | nestedParam path = 428 | NestedParam (String.join "." path) 429 | 430 | 431 | toQueryString : Params -> String 432 | toQueryString = 433 | normalizeParams 434 | >> List.map paramToString 435 | >> String.join "&" 436 | -------------------------------------------------------------------------------- /src/Postgrest/Internal/Requests.elm: -------------------------------------------------------------------------------- 1 | module Postgrest.Internal.Requests exposing 2 | ( Request(..) 3 | , RequestType(..) 4 | , defaultRequest 5 | , fullURL 6 | , mapRequest 7 | , requestTypeToBody 8 | , requestTypeToHTTPMethod 9 | , requestTypeToHeaders 10 | , setMandatoryParams 11 | , setTimeout 12 | ) 13 | 14 | import Http 15 | import Json.Decode exposing (Decoder) 16 | import Json.Encode as JE 17 | import Postgrest.Internal.Endpoint as Endpoint exposing (Endpoint) 18 | import Postgrest.Internal.JWT exposing (JWT, jwtHeader) 19 | import Postgrest.Internal.Params exposing (Params, concatParams, toQueryString) 20 | import Postgrest.Internal.URL exposing (BaseURL, baseURLToString) 21 | 22 | 23 | type Request r 24 | = Request (RequestOptions r) 25 | 26 | 27 | type alias RequestOptions r = 28 | { options : RequestType r 29 | , timeout : Maybe Float 30 | , defaultParams : Params 31 | , overrideParams : Params 32 | , mandatoryParams : Params 33 | , baseURL : BaseURL 34 | } 35 | 36 | 37 | type RequestType r 38 | = Post JE.Value (Decoder r) 39 | | Patch JE.Value (Decoder r) 40 | | Get (Decoder r) 41 | | Delete r 42 | 43 | 44 | defaultRequest : Endpoint b -> RequestType a -> Request a 45 | defaultRequest e requestType = 46 | Request 47 | { options = requestType 48 | , timeout = Nothing 49 | , defaultParams = Endpoint.defaultParams e 50 | , overrideParams = [] 51 | , mandatoryParams = [] 52 | , baseURL = Endpoint.url e 53 | } 54 | 55 | 56 | requestTypeToHeaders : JWT -> RequestType r -> List Http.Header 57 | requestTypeToHeaders jwt_ r = 58 | case r of 59 | Post _ _ -> 60 | [ jwtHeader jwt_, returnRepresentationHeader ] 61 | 62 | Patch _ _ -> 63 | [ jwtHeader jwt_, returnRepresentationHeader ] 64 | 65 | Get _ -> 66 | [ jwtHeader jwt_ ] 67 | 68 | Delete _ -> 69 | [ jwtHeader jwt_ 70 | 71 | -- Even though we don't need the record to be returned, this is a 72 | -- temporary workaround for when defaultSelect is specified, because 73 | -- if a select is specified without "Prefer" "return=representation" 74 | -- postgrest will give us an error that looks like this: 75 | -- 76 | -- { 77 | -- "hint": null, 78 | -- "details": null, 79 | -- "code": "42703", 80 | -- "message": "column pg_source.id does not exist" 81 | -- } 82 | , returnRepresentationHeader 83 | ] 84 | 85 | 86 | requestTypeToBody : RequestType r -> Http.Body 87 | requestTypeToBody r = 88 | case r of 89 | Delete _ -> 90 | Http.emptyBody 91 | 92 | Get _ -> 93 | Http.emptyBody 94 | 95 | Post body _ -> 96 | Http.jsonBody body 97 | 98 | Patch body _ -> 99 | Http.jsonBody body 100 | 101 | 102 | requestTypeToHTTPMethod : RequestType r -> String 103 | requestTypeToHTTPMethod r = 104 | case r of 105 | Post _ _ -> 106 | "POST" 107 | 108 | Patch _ _ -> 109 | "PATCH" 110 | 111 | Delete _ -> 112 | "DELETE" 113 | 114 | Get _ -> 115 | "GET" 116 | 117 | 118 | setMandatoryParams : Params -> Request a -> Request a 119 | setMandatoryParams p = 120 | mapRequest (\req -> { req | mandatoryParams = p }) 121 | 122 | 123 | returnRepresentationHeader : Http.Header 124 | returnRepresentationHeader = 125 | Http.header "Prefer" "return=representation" 126 | 127 | 128 | mapRequest : (RequestOptions a -> RequestOptions a) -> Request a -> Request a 129 | mapRequest f (Request options) = 130 | Request (f options) 131 | 132 | 133 | fullURL : RequestOptions r -> String 134 | fullURL { defaultParams, overrideParams, mandatoryParams, baseURL } = 135 | let 136 | params = 137 | concatParams [ defaultParams, overrideParams, mandatoryParams ] 138 | in 139 | [ baseURLToString baseURL, toQueryString params ] 140 | |> List.filter (String.isEmpty >> Basics.not) 141 | |> String.join "?" 142 | 143 | 144 | setTimeout : Float -> Request a -> Request a 145 | setTimeout t = 146 | mapRequest (\req -> { req | timeout = Just t }) 147 | -------------------------------------------------------------------------------- /src/Postgrest/Internal/URL.elm: -------------------------------------------------------------------------------- 1 | module Postgrest.Internal.URL exposing (BaseURL(..), baseURLToString) 2 | 3 | 4 | type BaseURL 5 | = BaseURL String 6 | 7 | 8 | baseURLToString : BaseURL -> String 9 | baseURLToString (BaseURL s) = 10 | s 11 | -------------------------------------------------------------------------------- /tests/PostgrestTests.elm: -------------------------------------------------------------------------------- 1 | module PostgrestTests exposing (suite) 2 | 3 | import Expect exposing (Expectation) 4 | import Postgrest.Client as P exposing (..) 5 | import Test exposing (..) 6 | 7 | 8 | matching = 9 | [ ( "age=lt.13", [ param "age" <| lt <| int 13 ] ) 10 | , ( "age=gte.18&student=is.true" 11 | , [ param "age" <| gte <| int 18 12 | , param "student" <| true 13 | ] 14 | ) 15 | , ( "or=(age.gte.14,age.lte.18)" 16 | , [ or 17 | [ param "age" <| gte <| int 14 18 | , param "age" <| lte <| int 18 19 | ] 20 | ] 21 | ) 22 | , ( "and=(grade.gte.90,student.is.true,or(age.gte.14,age.is.null))" 23 | , [ and 24 | [ param "grade" <| gte <| int 90 25 | , param "student" <| true 26 | , or 27 | [ param "age" <| gte <| int 14 28 | , param "age" <| null 29 | ] 30 | ] 31 | ] 32 | ) 33 | , ( "my_tsv=fts(french).amusant" 34 | , [ param "my_tsv" <| fts (Just "french") "amusant" 35 | ] 36 | ) 37 | , ( "my_tsv=plfts.The%20Fat%20Cats" 38 | , [ param "my_tsv" <| plfts Nothing "The Fat Cats" 39 | ] 40 | ) 41 | , ( "my_tsv=not.phfts(english).The%20Fat%20Cats" 42 | , [ param "my_tsv" <| P.not <| phfts (Just "english") "The Fat Cats" 43 | ] 44 | ) 45 | , ( "select=first_name,age" 46 | , [ select <| attributes [ "first_name", "age" ] ] 47 | ) 48 | , ( "select=fullName:full_name,birthDate:birth_date" 49 | , [ select <| 50 | [ attribute "fullName:full_name" 51 | , attribute "birthDate:birth_date" 52 | ] 53 | ] 54 | ) 55 | , ( "order=age.desc,height.asc" 56 | , [ order 57 | [ desc "age" 58 | , asc "height" 59 | ] 60 | ] 61 | ) 62 | , ( "order=age.asc.nullsfirst" 63 | , [ order 64 | [ asc "age" |> nullsfirst 65 | ] 66 | ] 67 | ) 68 | , ( "order=age.desc.nullslast" 69 | , [ order 70 | [ desc "age" |> nullslast 71 | ] 72 | ] 73 | ) 74 | , ( "limit=15&offset=30" 75 | , [ limit 15 76 | , offset 30 77 | ] 78 | ) 79 | , ( "select=title,directors(id,last_name)" 80 | , [ select 81 | [ attribute "title" 82 | , resource "directors" 83 | [ attribute "id" 84 | , attribute "last_name" 85 | ] 86 | ] 87 | ] 88 | ) 89 | , ( "select=*,roles(*)&roles.character=in.(\"Chico\",\"Harpo\",\"Groucho\")" 90 | , [ select 91 | [ attribute "*" 92 | , resource "roles" allAttributes 93 | ] 94 | , param "roles.character" <| inList string [ "Chico", "Harpo", "Groucho" ] 95 | ] 96 | ) 97 | , ( "select=*,roles(*)&roles.or=(character.eq.Gummo,character.eq.Zeppo)" 98 | , [ select 99 | [ attribute "*" 100 | , resource "roles" [ attribute "*" ] 101 | ] 102 | , nestedParam [ "roles" ] <| 103 | or 104 | [ param "character" <| eq <| string "Gummo" 105 | , param "character" <| eq <| string "Zeppo" 106 | ] 107 | ] 108 | ) 109 | , ( "select=*,actors(*)&actors.limit=10&actors.offset=2" 110 | , [ select 111 | [ attribute "*" 112 | , resource "actors" allAttributes 113 | ] 114 | , nestedParam [ "actors" ] <| limit 10 115 | , nestedParam [ "actors" ] <| offset 2 116 | ] 117 | ) 118 | , ( "a=like.a*c", [ param "a" <| like "a*c" ] ) 119 | , ( "a=ilike.a*c", [ param "a" <| ilike "a*c" ] ) 120 | , ( "foo=is.false", [ P.param "foo" P.false ] ) 121 | , ( "foo=is.true", [ P.param "foo" P.true ] ) 122 | , ( "foo=is.null", [ P.param "foo" P.null ] ) 123 | , ( "tag=cs.{foo,bar}", [ P.param "tag" <| P.contains <| List.map P.string [ "foo", "bar" ] ] ) 124 | , ( "tag=cd.{foo,bar}", [ P.param "tag" <| P.containedIn <| List.map P.string [ "foo", "bar" ] ] ) 125 | ] 126 | 127 | 128 | suite : Test 129 | suite = 130 | describe "operators" 131 | [ describe "documentation examples" 132 | (matching 133 | |> List.map 134 | (\( v, s ) -> 135 | test v <| 136 | \_ -> 137 | Expect.equal v (toQueryString s) 138 | ) 139 | ) 140 | , describe "nested param options" 141 | [ test "nested params options" <| 142 | \_ -> 143 | let 144 | actual = 145 | [ P.select 146 | [ P.resource "sites" 147 | [ P.resourceWithParams "streams" 148 | [ P.order [ P.asc "name" ] 149 | ] 150 | allAttributes 151 | ] 152 | ] 153 | ] 154 | |> toQueryString 155 | 156 | expected = 157 | "select=sites(streams(*))&sites.streams.order=name.asc" 158 | in 159 | Expect.equal actual expected 160 | ] 161 | , describe "combine params" 162 | [ test "last ones take precedent" <| 163 | \_ -> 164 | let 165 | sortParams = 166 | normalizeParams >> List.sortBy Tuple.first 167 | 168 | expected = 169 | [ P.param "a" <| eqString "1" 170 | , P.param "c" <| eqString "3" 171 | , P.param "b" <| eqString "5" 172 | ] 173 | |> sortParams 174 | 175 | eqString = 176 | P.eq << P.string 177 | 178 | set = 179 | [ [ P.param "a" <| eqString "1", P.param "b" <| eqString "2" ] 180 | , [ P.param "c" <| eqString "3", P.param "b" <| eqString "4" ] 181 | , [ P.param "b" <| eqString "5" ] 182 | ] 183 | 184 | actual = 185 | P.concatParams set 186 | |> sortParams 187 | in 188 | Expect.equal expected actual 189 | ] 190 | ] 191 | --------------------------------------------------------------------------------