Yagger / elm-odata4 / OData4.Url

Build Open Data Protocol (OData v4) queries in Elm.

This package supports a subset of OData 4.01 query option, common expression, and primitive literal.

http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html

http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part2-url-conventions.html

Example

import Time
import Url
import Url.Builder

-- Custom value function
dateAsString : Time.Posix -> Value
dateAsString posix =
    customValue (\s -> "'" ++ s ++ "'") (date posix)

[ filter
    ( or
        [ eq "City" (string "Tallinn")
        , eq "City" (string "Singapore")
        -- Custom operator
        , plainStringOperator "concat(concat(City,', '), Country) eq 'Berlin, Germany'"
        -- Custom value
        , ge "start/dateTime" (dateAsString (Time.millisToPosix 1631124861000))
        ]
    )
, top 20
, skip 40
, orderBy [ ("Created", Just desc), ("City", Just asc) ]
, select [ "Id", "City", "Created", "Body" ]
]
|> List.map toQueryParameter
-- Custom query option
|> List.append [ Url.Builder.string "$search" "blue OR green" ]
|> Url.Builder.toQuery >> Url.percentDecode
--> Just (String.join ""
--> [ "?$search=blue OR green&"
--> , "$filter=City eq 'Tallinn' or "
--> ,   "City eq 'Singapore' or "
--> ,   "concat(concat(City,', '), Country) eq 'Berlin, Germany' or "
--> ,   "start/dateTime ge '2021-09-08'&"
--> , "$top=20&"
--> , "$skip=40&"
--> , "$orderBy=Created desc,City asc&"
--> , "$select=Id,City,Created,Body"
--> ])

Query Options


type QueryOption

System query options are query string parameters that control the amount and order of the data returned for the resource identified by the URL.

select : List String -> QueryOption

The $select system query option requests that the service return only the properties explicitly requested by the client.

import Url
import Url.Builder

select [ "id", "subject", "body" ]
|> toQueryParameter >> List.singleton >> Url.Builder.toQuery >> Url.percentDecode
--> Just "?$select=id,subject,body"

filter : CommonExpression -> QueryOption

The $filter system query option restricts the set of items returned.

import Url
import Url.Builder

filter (endsWith "mail" "@hotmail.com")
|> toQueryParameter >> List.singleton >> Url.Builder.toQuery >> Url.percentDecode
--> Just "?$filter=endswith(mail,'@hotmail.com')"

orderBy : List ( String, Maybe Order ) -> QueryOption

The $orderby System Query option specifies the order in which items are returned from the service.

import Url
import Url.Builder

orderBy [ ("created", Just desc), ("displayName", Just asc) ]
|> toQueryParameter >> List.singleton >> Url.Builder.toQuery >> Url.percentDecode
--> Just "?$orderBy=created desc,displayName asc"

top : Basics.Int -> QueryOption

The $top query parameter requests the number of items in the queried collection to be included in the result. A client can request a particular page of items by combining $top and $skip.

import Url
import Url.Builder

top 10
|> toQueryParameter >> List.singleton >> Url.Builder.toQuery >> Url.percentDecode
--> Just "?$top=10"

skip : Basics.Int -> QueryOption

The $skip query parameter requests the number of items in the queried collection that are to be skipped and not included in the result. A client can request a particular page of items by combining $top and $skip.

import Url
import Url.Builder

skip 10
|> toQueryParameter >> List.singleton >> Url.Builder.toQuery >> Url.percentDecode
--> Just "?$skip=10"

Order


type Order

The expression can include the suffix asc for ascending or desc for descending. Defaults to asc.

asc : Order

Ascending order

import Url
import Url.Builder

orderBy [ ("displayName", Just asc) ]
|> toQueryParameter >> List.singleton >> Url.Builder.toQuery >> Url.percentDecode
--> Just "?$orderBy=displayName asc"

orderBy [ ("displayName", Nothing) ]
|> toQueryParameter >> List.singleton >> Url.Builder.toQuery >> Url.percentDecode
--> Just "?$orderBy=displayName"

desc : Order

Descending order

import Url
import Url.Builder

orderBy [ ("created", Just desc) ]
|> toQueryParameter >> List.singleton >> Url.Builder.toQuery >> Url.percentDecode
--> Just "?$orderBy=created desc"

orderBy [ ("created", Nothing) ]
|> toQueryParameter >> List.singleton >> Url.Builder.toQuery >> Url.percentDecode
--> Just "?$orderBy=created"

Common Expression Syntax [doc]


type CommonExpression

The following operators, functions, and literals can be used in $filter and $orderby

Logical Operators [doc]

eq : String -> Value -> CommonExpression

Equals

The null value is equal to itself, and only to itself.

import Url
import Url.Builder

filter (eq "name" (string "Luna"))
|> toQueryParameter >> List.singleton >> Url.Builder.toQuery >> Url.percentDecode
--> Just "?$filter=name eq 'Luna'"

ne : String -> Value -> CommonExpression

Not Equals

import Url
import Url.Builder

filter (ne "deleted" null)
|> toQueryParameter >> List.singleton >> Url.Builder.toQuery >> Url.percentDecode
--> Just "?$filter=deleted ne null"

gt : String -> Value -> CommonExpression

Greater Than

import Url
import Url.Builder

filter (gt "age" (int 14))
|> toQueryParameter >> List.singleton >> Url.Builder.toQuery >> Url.percentDecode
--> Just "?$filter=age gt 14"

ge : String -> Value -> CommonExpression

Greater Than or Equal

import Url
import Url.Builder

filter (ge "age" (int 14))
|> toQueryParameter >> List.singleton >> Url.Builder.toQuery >> Url.percentDecode
--> Just "?$filter=age ge 14"

lt : String -> Value -> CommonExpression

Less Than

import Url
import Url.Builder

filter (lt "age" (int 14))
|> toQueryParameter >> List.singleton >> Url.Builder.toQuery >> Url.percentDecode
--> Just "?$filter=age lt 14"

le : String -> Value -> CommonExpression

Less Than or Equal

import Url
import Url.Builder

filter (le "age" (int 14))
|> toQueryParameter >> List.singleton >> Url.Builder.toQuery >> Url.percentDecode
--> Just "?$filter=age le 14"

and : List CommonExpression -> CommonExpression

The and operator returns true if both the left and right operands evaluate to true, otherwise it returns false.

The null value is treated as unknown, so if one operand evaluates to null and the other operand to false, the and operator returns false. All other combinations with null return null.

import Url
import Url.Builder

filter (and [ eq "name" (string "Luna"), le "age" (int 14)])
|> toQueryParameter >> List.singleton >> Url.Builder.toQuery >> Url.percentDecode
--> Just "?$filter=name eq 'Luna' and age le 14"

or : List CommonExpression -> CommonExpression

The or operator returns false if both the left and right operands both evaluate to false, otherwise it returns true.

The null value is treated as unknown, so if one operand evaluates to null and the other operand to true, the or operator returns true. All other combinations with null return null.

import Url
import Url.Builder

filter (or [ eq "department" (string "Sales"), eq "department" (string "Marketing")])
|> toQueryParameter >> List.singleton >> Url.Builder.toQuery >> Url.percentDecode
--> Just "?$filter=department eq 'Sales' or department eq 'Marketing'"

not_ : CommonExpression -> CommonExpression

The not operator returns true if the operand returns false, otherwise it returns false.

The null value is treated as unknown, so not null returns null.

import Url
import Url.Builder

filter (not_ (contains "email" "@org.com"))
|> toQueryParameter >> List.singleton >> Url.Builder.toQuery >> Url.percentDecode
--> Just "?$filter=not contains(email,'@org.com')"

in_ : String -> List Value -> CommonExpression

The in operator returns true if the left operand is a member of the right operand.

The right operand is a comma-separated list of primitive values.

import Url
import Url.Builder

filter (in_ "department" [string "Retail", string "Sales"])
|> toQueryParameter >> List.singleton >> Url.Builder.toQuery >> Url.percentDecode
--> Just "?$filter=department in ('Retail', 'Sales')"

String Functions [doc]

startsWith : String -> String -> CommonExpression

The startsWith function returns true if the first string starts with the second string, otherwise it returns false

import Url
import Url.Builder

filter (startsWith "CompanyName" "Alfr")
|> toQueryParameter >> List.singleton >> Url.Builder.toQuery >> Url.percentDecode
--> Just "?$filter=startswith(CompanyName,'Alfr')"

endsWith : String -> String -> CommonExpression

The endsWith function returns true if the first string ends with the second string, otherwise it returns false

import Url
import Url.Builder

filter (endsWith "CompanyName" "Futterkiste")
|> toQueryParameter >> List.singleton >> Url.Builder.toQuery >> Url.percentDecode
--> Just "?$filter=endswith(CompanyName,'Futterkiste')"

contains : String -> String -> CommonExpression

The contains function returns true if the second string is a substring of the first string, otherwise it returns false

import Url
import Url.Builder

filter (contains "CompanyName" "Alfreds")
|> toQueryParameter >> List.singleton >> Url.Builder.toQuery >> Url.percentDecode
--> Just "?$filter=contains(CompanyName,'Alfreds')"

Lambda Operators [doc]

any : String -> CommonExpression -> CommonExpression

The any operator applies a Boolean expression to each member of a collection and returns true if the expression is true for any member of the collection, otherwise it returns false. The any operator without an argument returns true if the collection is not empty.

import Url
import Url.Builder

filter (any "Items" (gt "Quantity" (int 100)))
|> toQueryParameter >> List.singleton >> Url.Builder.toQuery >> Url.percentDecode
--> Just "?$filter=Items/any(a:a/Quantity gt 100)"

all : String -> CommonExpression -> CommonExpression

The all operator applies a Boolean expression to each member of a collection and returns true if the expression is true for all members of the collection, otherwise it returns false.

import Url
import Url.Builder

filter (all "Items" (gt "Quantity" (int 100)))
|> toQueryParameter >> List.singleton >> Url.Builder.toQuery >> Url.percentDecode
--> Just "?$filter=Items/all(a:a/Quantity gt 100)"

Primitive Literals [doc]


type Value

Value type represents primitive literals

NullValue eq null
TrueValue eq true
FalseValue eq false
IntegerValue lt -128
FloatValue eq 34.95
StringValue eq 'Say Hello,then go'
DateValue eq 2012-12-03
DateTimeOffsetValue eq 2012-12-03T07:16:23Z
GuidValue eq 01234567-89ab-cdef-0123-456789abcdef

null : Value

null
|> test.stringFromValue
--> "null"

true : Value

true
|> test.stringFromValue
--> "true"

false : Value

false
|> test.stringFromValue
--> "false"

int : Basics.Int -> Value

int -128
|> test.stringFromValue
--> "-128"

float : Basics.Float -> Value

float 0.31415
|> test.stringFromValue
--> "0.31415"

string : String -> Value

Note the single quotes

string "Say Hello,then go"
|> test.stringFromValue
--> "'Say Hello,then go'"

date : Time.Posix -> Value

import Time

date (Time.millisToPosix 1631124861000)
|> test.stringFromValue
--> "2021-09-08"

dateTime : Time.Posix -> Value

import Time

dateTime (Time.millisToPosix 1631124861000)
|> test.stringFromValue
--> "2021-09-08T18:14:21Z"

guid : String -> Value

guid "01234567-89ab-cdef-0123-456789abcdef"
|> test.stringFromValue
--> "01234567-89ab-cdef-0123-456789abcdef"

Custom

plainStringOperator : String -> CommonExpression

plainStringOperator is useful

In the following example, concat is not implemented in this package.

import Url
import Url.Builder

filter
    ( or
        [ eq "Country" (string "France")
        , plainStringOperator "concat(concat(City,', '), Country) eq 'Berlin, Germany'"
        ]
    )
|> toQueryParameter >> List.singleton >> Url.Builder.toQuery >> Url.percentDecode
--> Just "?$filter=Country eq 'France' or concat(concat(City,', '), Country) eq 'Berlin, Germany'"

customValue : (String -> String) -> Value -> Value

In situations like this https://github.com/microsoftgraph/microsoft-graph-docs/issues/14547, custom value may come in handy. In the example below, date needs to be treated as string (with single quotes):

import Time

dateAsString : Time.Posix -> Value
dateAsString posix =
    customValue (\s -> "'" ++ s ++ "'") (date posix)

dateAsString (Time.millisToPosix 1631124861000)
|> test.stringFromValue
--> "'2021-09-08'"

Building Url.Builder.QueryParameter

toQueryParameter : QueryOption -> Url.Builder.QueryParameter

Build Url.Builder.QueryParameter from QueryOption, which you can further combine with other QueryParameters and convert to string using Url.Builder.toQuery

import Url
import Url.Builder

[ toQueryParameter (top 10) ]
|> Url.Builder.toQuery >> Url.percentDecode
--> Just "?$top=10"

Test internals

test : { stringFromValue : Value -> String }

Test internals are needed for elm-verify-examples