packages feed

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