airtable-api 0.2.0.1 → 0.2.1.1
raw patch · 3 files changed
+97/−35 lines, 3 filesPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
API changes (from Hackage documentation)
- Airtable.Query: getTable :: (FromJSON a) => AirtableOptions -> TableName -> IO (Table a)
- Airtable.Table: toRec :: IsRecord a => a -> RecordID
+ Airtable.Query: airtableOptionsToWreqOptions :: AirtableOptions -> Options
+ Airtable.Query: createRecord :: (HasCallStack, ToJSON a, FromJSON a) => AirtableOptions -> TableName -> a -> IO (Record a)
+ Airtable.Query: deleteRecord :: AirtableOptions -> TableName -> RecordID -> IO ()
+ Airtable.Query: getRecords :: (HasCallStack, FromJSON a) => AirtableOptions -> TableName -> IO (Table a)
+ Airtable.Query: tableNameToUrl :: AirtableOptions -> TableName -> String
+ Airtable.Query: updateRecord :: (ToJSON a) => AirtableOptions -> TableName -> RecordID -> a -> IO ()
+ Airtable.Table: Record :: RecordID -> a -> UTCTime -> Record a
+ Airtable.Table: [createdTime] :: Record a -> UTCTime
+ Airtable.Table: [recordId] :: Record a -> RecordID
+ Airtable.Table: [recordObj] :: Record a -> a
+ Airtable.Table: data Record a
+ Airtable.Table: instance Airtable.Table.IsRecord (Airtable.Table.Record a)
+ Airtable.Table: instance Data.Aeson.Types.FromJSON.FromJSON a => Data.Aeson.Types.FromJSON.FromJSON (Airtable.Table.Record a)
+ Airtable.Table: toRecId :: IsRecord a => a -> RecordID
Files
- airtable-api.cabal +1/−1
- src/Airtable/Query.hs +72/−21
- src/Airtable/Table.hs +24/−13
airtable-api.cabal view
@@ -1,5 +1,5 @@ name: airtable-api-version: 0.2.0.1+version: 0.2.1.1 synopsis: Requesting and introspecting Tables within an Airtable project. -- description: homepage: https://github.com/ooblahman/airtable-api
src/Airtable/Query.hs view
@@ -5,21 +5,31 @@ -- * Configuration for Airtable requests. , AirtableOptions(..) , defaultAirtableOptions- -- * Main API- , getTable+ , airtableOptionsToWreqOptions+ , tableNameToUrl+ -- * API+ , getRecords+ , createRecord+ , updateRecord+ , deleteRecord ) where import Airtable.Table +import GHC.Stack import Network.Wreq import Control.Lens ((^.), (.~), (&))+import Control.Monad (void) -import Data.Aeson (FromJSON, ToJSON, eitherDecode)+import Data.Aeson (FromJSON, ToJSON, eitherDecode, toJSON) import Data.Monoid+import Data.ByteString.Lazy (ByteString) import qualified Data.ByteString.Char8 as BC +-- * Configuration for Airtable requests.+ -- | Options data AirtableOptions = AirtableOptions { -- | the API key for your project@@ -43,21 +53,39 @@ base_url :: String base_url = "https://api.airtable.com/" --- | Retrieve a table from airtable.com given its name. Handles pagination correctly.-getTable :: (FromJSON a) => AirtableOptions -> TableName -> IO (Table a)-getTable opts tname = getTableFromUrl net_otps url+-- | Produce Wreq options from 'AirtableOptions'+airtableOptionsToWreqOptions :: AirtableOptions -> Options+airtableOptionsToWreqOptions opts = + defaults & header "Authorization" .~ ["Bearer " <> BC.pack (apiKey opts)]++-- | Produce a request URL to a table.+tableNameToUrl :: AirtableOptions -> TableName -> String+tableNameToUrl opts tname = + base_url+ <> "v"+ <> show (apiVersion opts)+ <> "/"+ <> appId opts+ <> "/"+ <> tname++-- * API++fromResp :: (HasCallStack, FromJSON a) => Response ByteString -> a+fromResp r = decoder $ r ^. responseBody where- net_otps = defaults & header "Authorization" .~ ["Bearer " <> BC.pack (apiKey opts)]- url = base_url- <> "v"- <> show (apiVersion opts)- <> "/"- <> appId opts- <> "/"- <> tname+ decoder b = case eitherDecode b of+ Left e -> error e+ Right r -> r -getTableFromUrl :: (FromJSON a) => Options -> String -> IO (Table a)-getTableFromUrl opts url = do+-- | Retrieve the records for a table from airtable.com given its name. Handles pagination correctly.+getRecords :: (HasCallStack, FromJSON a) => AirtableOptions -> TableName -> IO (Table a)+getRecords opts tname = + getRecordsFromUrl (airtableOptionsToWreqOptions opts) (tableNameToUrl opts tname)++-- | Retrieve the records for a table given a URL and network options.+getRecordsFromUrl :: (HasCallStack, FromJSON a) => Options -> String -> IO (Table a)+getRecordsFromUrl opts url = do resp <- getWith opts url getMore (fromResp resp) where@@ -68,8 +96,31 @@ Nothing -> pure tbl - fromResp r = decoder $ r ^. responseBody- where- decoder b = case eitherDecode b of- Left e -> error $ e <> "\nSource string: " <> show b- Right r -> r+-- | Upload a record to a given table. Returns the newly created `RecordID`.+createRecord :: (HasCallStack, ToJSON a, FromJSON a) => AirtableOptions -> TableName -> a -> IO (Record a)+createRecord opts tname a = do+ resp <- postWith netOpts url payload+ return $ fromResp resp+ where+ url = tableNameToUrl opts tname+ netOpts = airtableOptionsToWreqOptions opts + payload = toJSON a++-- | Update a record on a given table, using the supplied fields ('a').+updateRecord :: (ToJSON a) => AirtableOptions -> TableName -> RecordID -> a -> IO ()+updateRecord opts tname recId a = do+ void $ + customPayloadMethodWith "PATCH" netOpts url payload+ where+ url = tableNameToUrl opts tname <> "/" <> rec2str recId+ netOpts = airtableOptionsToWreqOptions opts+ payload = toJSON a++-- | Delete a record on a table. +deleteRecord :: AirtableOptions -> TableName -> RecordID -> IO ()+deleteRecord opts tname recId = + void $ deleteWith netOpts url + where+ netOpts = airtableOptionsToWreqOptions opts+ url = tableNameToUrl opts tname <> "/" <> rec2str recId+
src/Airtable/Table.hs view
@@ -7,8 +7,10 @@ module Airtable.Table (+ -- * Record+ Record(..) -- * RecordID- RecordID(..)+ , RecordID(..) , rec2str -- * IsRecord class , IsRecord(..)@@ -67,16 +69,28 @@ -- | A convenience typeclass for selecting records using RecordID-like keys. class IsRecord a where- toRec :: a -> RecordID+ toRecId :: a -> RecordID instance IsRecord RecordID where- toRec = id+ toRecId = id instance IsRecord String where- toRec = RecordID . T.pack+ toRecId = RecordID . T.pack -- * Table +-- | An airtable record.+data Record a = Record { recordId :: RecordID, recordObj :: a, createdTime :: UTCTime }++instance (FromJSON a) => FromJSON (Record a) where+ parseJSON = withObject "record object" $ \v -> do+ Record <$> v .: "id" + <*> v .: "fields"+ <*> v .: "createdTime"++instance IsRecord (Record a) where+ toRecId = recordId+ -- | Airtable's table type data Table a = Table { tableRecords :: Map.HashMap RecordID a , tableOffset :: Maybe Text@@ -92,27 +106,24 @@ type TableName = String instance (FromJSON a) => FromJSON (Table a) where- parseJSON (Object v) = do+ parseJSON = withObject "table object" $ \v -> do recs <- v .: "records" :: Parser [Value] parsedRecs <- foldlM parseRec Map.empty recs offset <- v .:? "offset" return $ Table parsedRecs offset where- parseRec tbl json@(Object v) =+ parseRec tbl = withObject "record object" $ \v -> do recId <- v .: "id" fields <- v .: "fields" obj <- parseJSON fields return $ Map.insert recId obj tbl- parseRec tbl invalid = typeMismatch "Table" invalid- parseJSON invalid = typeMismatch "Table" invalid parseRecord :: (UTCTime -> RecordID -> Value -> Parser a) -> Value -> Parser a-parseRecord action (Object v) = do+parseRecord action = withObject "record object" $ \v -> do created <- v .: "createdTime" recordId <- v .: "id" fields <- v .: "fields" action created recordId fields-parseRecord _action invalid = typeMismatch "parseFields" invalid parseFields :: (Value -> Parser a) -> Value -> Parser a parseFields action (Object v) = v .: "fields" >>= action@@ -130,12 +141,12 @@ -- | Check if a record exists at the given key in a table. exists :: (IsRecord r) => Table a -> r -> Bool-exists tbl rec = Map.member (toRec rec) (tableRecords tbl)+exists tbl rec = Map.member (toRecId rec) (tableRecords tbl) -- | Unsafely lookup a record using its RecordID. Will throw a pretty-printed error -- if record does not exist. select :: (HasCallStack, IsRecord r, Show a) => Table a -> r -> a-select tbl rec = tableRecords tbl `lookup` toRec rec+select tbl rec = tableRecords tbl `lookup` toRecId rec where lookup mp k = case Map.lookup k mp of Just v -> v@@ -143,7 +154,7 @@ -- | Safely lookup a record using its RecordID. selectMaybe :: (IsRecord r) => Table a -> r -> Maybe a-selectMaybe tbl rec = toRec rec `Map.lookup` tableRecords tbl+selectMaybe tbl rec = toRecId rec `Map.lookup` tableRecords tbl -- | Read all records. selectAll :: Table a -> [a]