firebase-hs-0.1.0.0: src/Firebase/Firestore/Query.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE StrictData #-}
-- |
-- Module : Firebase.Firestore.Query
-- Description : Structured query DSL for Firestore
-- License : MIT
--
-- A pure builder-pattern DSL for constructing Firestore structured queries.
-- Compose with @(&)@ from "Data.Function":
--
-- @
-- import Data.Function ((&))
--
-- let q = query (CollectionPath \"users\")
-- & where_ (fieldFilter \"age\" OpGreaterThan (IntegerValue 18))
-- & orderBy \"age\" Ascending
-- & limit 10
-- @
module Firebase.Firestore.Query
( -- * Query Construction
StructuredQuery,
query,
where_,
orderBy,
limit,
offset,
-- * Filters
Filter (..),
FieldFilter (..),
CompositeFilter (..),
CompositeOp (..),
FilterOp (..),
fieldFilter,
compositeAnd,
compositeOr,
-- * Ordering
OrderDirection (..),
-- * Encoding
encodeQuery,
)
where
import Data.Aeson ((.=))
import qualified Data.Aeson as Aeson
import Data.Text (Text)
import Firebase.Firestore.Types (CollectionPath (..), FirestoreValue)
-- ---------------------------------------------------------------------------
-- Types
-- ---------------------------------------------------------------------------
-- | A structured query builder. Construct with 'query' and refine with
-- 'where_', 'orderBy', 'limit', and 'offset'.
data StructuredQuery = StructuredQuery
{ sqFrom :: !CollectionPath,
sqWhere :: !(Maybe Filter),
sqOrderBy :: ![(Text, OrderDirection)],
sqLimit :: !(Maybe Int),
sqOffset :: !(Maybe Int)
}
deriving (Eq, Show)
-- | A query filter: either a single field filter or a composite of filters.
data Filter
= FieldFilterF !FieldFilter
| CompositeFilterF !CompositeFilter
deriving (Eq, Show)
-- | A filter on a single field.
data FieldFilter = FieldFilter
{ ffField :: !Text,
ffOp :: !FilterOp,
ffValue :: !FirestoreValue
}
deriving (Eq, Show)
-- | A composite of multiple filters joined by AND or OR.
data CompositeFilter = CompositeFilter
{ cfOp :: !CompositeOp,
cfFilters :: ![Filter]
}
deriving (Eq, Show)
-- | Composite filter operator.
data CompositeOp = OpAnd | OpOr
deriving (Eq, Show)
-- | Field filter comparison operators.
data FilterOp
= OpEqual
| OpNotEqual
| OpLessThan
| OpLessThanOrEqual
| OpGreaterThan
| OpGreaterThanOrEqual
| OpArrayContains
| OpIn
| OpArrayContainsAny
| OpNotIn
deriving (Eq, Show)
-- | Sort direction for 'orderBy'.
data OrderDirection = Ascending | Descending
deriving (Eq, Show)
-- ---------------------------------------------------------------------------
-- Builders
-- ---------------------------------------------------------------------------
-- | Start a query on a collection.
query :: CollectionPath -> StructuredQuery
query cp =
StructuredQuery
{ sqFrom = cp,
sqWhere = Nothing,
sqOrderBy = [],
sqLimit = Nothing,
sqOffset = Nothing
}
-- | Add a filter to the query. Replaces any existing filter.
where_ :: Filter -> StructuredQuery -> StructuredQuery
where_ f sq = sq {sqWhere = Just f}
-- | Add an ordering clause. Multiple calls append in order.
orderBy :: Text -> OrderDirection -> StructuredQuery -> StructuredQuery
orderBy field dir sq = sq {sqOrderBy = sqOrderBy sq ++ [(field, dir)]}
-- | Set the maximum number of results to return.
limit :: Int -> StructuredQuery -> StructuredQuery
limit n sq = sq {sqLimit = Just n}
-- | Set the number of results to skip before returning.
offset :: Int -> StructuredQuery -> StructuredQuery
offset n sq = sq {sqOffset = Just n}
-- | Convenience: create a single field filter.
fieldFilter :: Text -> FilterOp -> FirestoreValue -> Filter
fieldFilter field op val = FieldFilterF (FieldFilter field op val)
-- | Convenience: create an AND composite filter.
compositeAnd :: [Filter] -> Filter
compositeAnd = CompositeFilterF . CompositeFilter OpAnd
-- | Convenience: create an OR composite filter.
compositeOr :: [Filter] -> Filter
compositeOr = CompositeFilterF . CompositeFilter OpOr
-- ---------------------------------------------------------------------------
-- JSON Encoding
-- ---------------------------------------------------------------------------
-- | Encode a 'StructuredQuery' to the JSON format expected by the
-- Firestore REST API's @:runQuery@ endpoint.
encodeQuery :: StructuredQuery -> Aeson.Value
encodeQuery sq =
Aeson.object
[ "structuredQuery"
.= Aeson.object
( ["from" .= [encodeCollectionSelector (sqFrom sq)]]
++ maybe [] (\f -> ["where" .= encodeFilter f]) (sqWhere sq)
++ encodeOrderBy (sqOrderBy sq)
++ maybe [] (\n -> ["limit" .= n]) (sqLimit sq)
++ maybe [] (\n -> ["offset" .= n]) (sqOffset sq)
)
]
-- | Encode a collection selector.
encodeCollectionSelector :: CollectionPath -> Aeson.Value
encodeCollectionSelector (CollectionPath cp) =
Aeson.object ["collectionId" .= cp]
-- | Encode a filter to Firestore JSON.
encodeFilter :: Filter -> Aeson.Value
encodeFilter (FieldFilterF ff) =
Aeson.object
[ "fieldFilter"
.= Aeson.object
[ "field" .= Aeson.object ["fieldPath" .= ffField ff],
"op" .= encodeFilterOp (ffOp ff),
"value" .= ffValue ff
]
]
encodeFilter (CompositeFilterF cf) =
Aeson.object
[ "compositeFilter"
.= Aeson.object
[ "op" .= encodeCompositeOp (cfOp cf),
"filters" .= map encodeFilter (cfFilters cf)
]
]
-- | Encode ordering clauses.
encodeOrderBy :: [(Text, OrderDirection)] -> [(Aeson.Key, Aeson.Value)]
encodeOrderBy [] = []
encodeOrderBy orders =
[ "orderBy"
.= [ Aeson.object
[ "field" .= Aeson.object ["fieldPath" .= field],
"direction" .= encodeDirection dir
]
| (field, dir) <- orders
]
]
-- | Encode a filter operator to its Firestore string representation.
encodeFilterOp :: FilterOp -> Text
encodeFilterOp OpEqual = "EQUAL"
encodeFilterOp OpNotEqual = "NOT_EQUAL"
encodeFilterOp OpLessThan = "LESS_THAN"
encodeFilterOp OpLessThanOrEqual = "LESS_THAN_OR_EQUAL"
encodeFilterOp OpGreaterThan = "GREATER_THAN"
encodeFilterOp OpGreaterThanOrEqual = "GREATER_THAN_OR_EQUAL"
encodeFilterOp OpArrayContains = "ARRAY_CONTAINS"
encodeFilterOp OpIn = "IN"
encodeFilterOp OpArrayContainsAny = "ARRAY_CONTAINS_ANY"
encodeFilterOp OpNotIn = "NOT_IN"
-- | Encode a composite operator.
encodeCompositeOp :: CompositeOp -> Text
encodeCompositeOp OpAnd = "AND"
encodeCompositeOp OpOr = "OR"
-- | Encode an order direction.
encodeDirection :: OrderDirection -> Text
encodeDirection Ascending = "ASCENDING"
encodeDirection Descending = "DESCENDING"