Yagger/elm-odata4/2.0.1

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

  • If you need a query option, common expression, or primitive literal that is missing in this package undeservedly, suggest a PR
  • If what you need is missing in this package and it's ok, or it deviates from the standard, or other, then for:

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

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"

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 : 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"

Order

type Order

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

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"

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]

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"

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"

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'"

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]

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)"

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
|> test.stringFromValue
--> "null"
true
|> test.stringFromValue
--> "true"
false
|> test.stringFromValue
--> "false"
int : Int -> Value
int -128
|> test.stringFromValue
--> "-128"
float : 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 : Posix -> Value
import Time


date (Time.millisToPosix 1631124861000)
|> test.stringFromValue
--> "2021-09-08"
dateTime : 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 is useful

  • when your target API offers OData operator that is out of the standard scope

  • when your target API OData formatting differs from standard formatting

  • for OData operators that are not implemented by this package

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 -> 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