/postgrest-client

Construct postgrest requests in Elm

Primary LanguageElmMIT LicenseMIT

alex-tan/postgrest-client

Build Status

This package allows you to both easily construct Postgrest query strings and also make postgrest requests using Elm.

This library allows you to construct and execute requests in a typesafe manner, with little boilerplate. Here's what a full example might look like:

module Api.People exposing (delete, getMany, post)

import Json.Decode exposing (..)
import Json.Encode as JE
import Postgrest.Client as P

-- Optional, but recommended to have a type that
-- represents your primary key.
type PersonID
    = PersonID Int


-- And a way to unwrap it...
personID : PersonID -> Int
personID (PersonID id) =
    id


-- Define the record you would fetch back from the server.
type alias Person =
    { id : PersonID
    , name : String
    }


-- Define a submission record, without the primary key.
type alias PersonSubmission =
    { name : String
    }


-- Decoders are written using Json.Decode
decodeUnit : Decoder Person
decodeUnit =
    map2 Person
        (field "id" <| map PersonID int)
        (field "name" string)


-- Encoders are written using Json.Encode
encode : PersonSubmission -> JE.Value
encode person =
    JE.object
        [ ( "name", JE.string person.name )
        ]


-- Tell Postgrest.Client the column name of your primary key and
-- how to convert it into a parameter.
primaryKey : P.PrimaryKey PersonID
primaryKey =
    P.primaryKey ( "id", P.int << personID )


-- Tell Postgrest.Client the URL of the postgrest endpoint and how
-- to decode records from it.
endpoint : P.Endpoint Person
endpoint =
    P.endpoint "/people" decodeUnit


-- Fetch many records. If you want to specify parameters use `setParams`
getMany : P.Request (List Person)
getMany =
    P.getMany endpoint


-- Delete by primary key. This is a convenience function that reduces
-- the likelihood that you delete the wrong records by specifying incorrect
-- parameters.
delete : PersonID -> P.Request PersonID
delete =
    P.deleteByPrimaryKey endpoint primaryKey


-- Create a record.
post : PersonSubmission -> P.Request Person
post =
    P.postOne endpoint << encode

Here's how you could use it:

import Api.People as People
import Postgrest.Client as P

jwt : P.JWT
jwt =
    P.jwt "myjwt"

type Msg 
    = PersonCreated (Result P.Error Person)
    | PeopleLoaded (Result P.Error (List Person))
    | PersonDeleted (Result P.Error PersonID)

toCmd = 
    P.toCmd jwt

cmdExamples =
    [ People.post
        { name = "Yasujirō Ozu"
        }
        |> P.toCmd jwt PersonCreated
    , People.getMany
        |> P.setParams [ P.order [ P.asc "name" ], P.limit 10 ]
        |> toCmd PeopleLoaded
    , Person.delete personID
        |> toCmd PersonDeleted
    ]

Most query operators are currently supported:

View Full Documentation

URL Query Construction

Using select

If you're not selecting any nested resources in your request, you can use attributes:

-- "select=id,name"
P.toQueryString
    [ P.select <| P.attributes [ "id", "name" ]
    ]

If you want to select attributes and resources, you can use the attribute and resource functions:

-- select=id,name,grades(percentage)
P.toQueryString
  [ P.select
      [ P.attribute "id"
      , P.attribute "name"
      , P.resource "grades"
          [ P.attribute "percentage"
          ]
      ]
  ]

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:

-- select=id,name,grades(percentage)&grades.order=percentage.desc&grades.limit=10
P.toQueryString
  [ P.select
      [ P.attribute "id"
      , P.attribute "name"
      , P.resourceWithParams "grades"
          [ P.order [ P.desc "percentage" ], P.limit 10 ]
          [ P.attribute "percentage"
          ]
      ]
  ]

Conditions

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:

-- student_id=eq.100&grade=gte.90&or=(self_evaluation.gte.90,self_evaluation.is.null)
P.toQueryString
  [ P.param "student_id" <| P.eq <| P.int 100
  , P.param "grade" <| P.gte <| P.int 90
  , P.or
      [ P.param "self_evaluation" <| P.gte <| P.int 90
      , P.param "self_evaluation" P.null
      ]
  ]

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.

-- name=in.("Chico","Harpo","Groucho")
P.toQueryString
  [ P.param "name" <| P.inList P.string [ "Chico", "Harpo", "Groucho" ]
  ]

Order

You can order results by multiple columns as well as using nullsfirst or nullslast.

-- order=age.asc.nullsfirst,created_at.desc
P.toQueryString
  [ P.order
      [ P.asc "age" |> P.nullsfirst
      , P.desc "created_at"
      ]
  ]

Combining Params

Maybe you have default parameters that you want to reuse across multiple functions. You can combine them using combineParams:

defaultParams : P.Params
defaultParams =
    [ P.select <| P.attributes [ "id", "name" ]
    , P.limit 10
    ]


constructParams : P.Params -> P.Params
constructParams =
    P.combineParams defaultParams


-- limit=100&select=id,name
constructParams [ P.limit 100 ]

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.