airtable-api 0.2.1.1 → 0.3.2.2
raw patch · 4 files changed
+74/−67 lines, 4 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
- Airtable.Query: airtableOptionsToWreqOptions :: AirtableOptions -> Options
- Airtable.Table: class IsRecord a
- Airtable.Table: instance Airtable.Table.IsRecord (Airtable.Table.Record a)
- Airtable.Table: instance Airtable.Table.IsRecord Airtable.Table.RecordID
- Airtable.Table: instance Airtable.Table.IsRecord GHC.Base.String
- Airtable.Table: parseFields :: (Value -> Parser a) -> Value -> Parser a
- Airtable.Table: parseRecord :: (UTCTime -> RecordID -> Value -> Parser a) -> Value -> Parser a
+ Airtable.Query: getRecordsFromUrl :: (HasCallStack, FromJSON a) => Options -> String -> IO (Table a)
+ Airtable.Query: mkWreqOptions :: AirtableOptions -> Options
+ Airtable.Query: respJsonBody :: (HasCallStack, FromJSON a) => Response ByteString -> a
+ Airtable.Table: class HasRecordId a
+ Airtable.Table: instance Airtable.Table.HasRecordId (Airtable.Table.Record a)
+ Airtable.Table: instance Airtable.Table.HasRecordId Airtable.Table.RecordID
+ Airtable.Table: instance Airtable.Table.HasRecordId GHC.Base.String
+ Airtable.Table: instance Data.Aeson.Types.ToJSON.ToJSON Airtable.Table.RecordID
+ Airtable.Table: instance Data.Aeson.Types.ToJSON.ToJSON a => Data.Aeson.Types.ToJSON.ToJSON (Airtable.Table.Record a)
+ Airtable.Table: instance GHC.Base.Functor Airtable.Table.Record
+ Airtable.Table: instance GHC.Classes.Eq a => GHC.Classes.Eq (Airtable.Table.Record a)
+ Airtable.Table: instance GHC.Classes.Ord a => GHC.Classes.Ord (Airtable.Table.Record a)
+ Airtable.Table: instance GHC.Generics.Generic (Airtable.Table.Record a)
+ Airtable.Table: instance GHC.Read.Read a => GHC.Read.Read (Airtable.Table.Record a)
+ Airtable.Table: instance GHC.Show.Show a => GHC.Show.Show (Airtable.Table.Record a)
- Airtable.Table: Table :: HashMap RecordID a -> Maybe Text -> Table a
+ Airtable.Table: Table :: HashMap RecordID (Record a) -> Maybe Text -> Table a
- Airtable.Table: [tableRecords] :: Table a -> HashMap RecordID a
+ Airtable.Table: [tableRecords] :: Table a -> HashMap RecordID (Record a)
- Airtable.Table: deleteWhere :: Table a -> (RecordID -> a -> Bool) -> Table a
+ Airtable.Table: deleteWhere :: Table a -> (Record a -> Bool) -> Table a
- Airtable.Table: exists :: (IsRecord r) => Table a -> r -> Bool
+ Airtable.Table: exists :: (HasRecordId r) => Table a -> r -> Bool
- Airtable.Table: select :: (HasCallStack, IsRecord r, Show a) => Table a -> r -> a
+ Airtable.Table: select :: (HasCallStack, HasRecordId r, Show a) => Table a -> r -> Record a
- Airtable.Table: selectAll :: Table a -> [a]
+ Airtable.Table: selectAll :: Table a -> [Record a]
- Airtable.Table: selectKeyWhere :: Table a -> (RecordID -> a -> Bool) -> [RecordID]
+ Airtable.Table: selectKeyWhere :: Table a -> (Record a -> Bool) -> [RecordID]
- Airtable.Table: selectMaybe :: (IsRecord r) => Table a -> r -> Maybe a
+ Airtable.Table: selectMaybe :: (HasRecordId r) => Table a -> r -> Maybe (Record a)
- Airtable.Table: selectWhere :: Table a -> (RecordID -> a -> Bool) -> [a]
+ Airtable.Table: selectWhere :: Table a -> (Record a -> Bool) -> [Record a]
- Airtable.Table: toList :: Table a -> [(RecordID, a)]
+ Airtable.Table: toList :: Table a -> [(RecordID, Record a)]
- Airtable.Table: toRecId :: IsRecord a => a -> RecordID
+ Airtable.Table: toRecId :: HasRecordId a => a -> RecordID
Files
- README.md +1/−1
- airtable-api.cabal +1/−1
- src/Airtable/Query.hs +22/−18
- src/Airtable/Table.hs +50/−47
README.md view
@@ -8,6 +8,6 @@ ... - location: git: https://github.com/ooblahman/airtable-api.git- commit: 6895cbbc4f83069a8455fcc00f6e313d867ca296+ commit: HEAD extra-dep: true ```
airtable-api.cabal view
@@ -1,5 +1,5 @@ name: airtable-api-version: 0.2.1.1+version: 0.3.2.2 synopsis: Requesting and introspecting Tables within an Airtable project. -- description: homepage: https://github.com/ooblahman/airtable-api
src/Airtable/Query.hs view
@@ -5,13 +5,16 @@ -- * Configuration for Airtable requests. , AirtableOptions(..) , defaultAirtableOptions- , airtableOptionsToWreqOptions+ , mkWreqOptions , tableNameToUrl -- * API , getRecords+ , getRecordsFromUrl , createRecord , updateRecord , deleteRecord+ -- * Low-level helpers+ , respJsonBody ) where @@ -54,8 +57,8 @@ base_url = "https://api.airtable.com/" -- | Produce Wreq options from 'AirtableOptions'-airtableOptionsToWreqOptions :: AirtableOptions -> Options-airtableOptionsToWreqOptions opts = +mkWreqOptions :: AirtableOptions -> Options+mkWreqOptions opts = defaults & header "Authorization" .~ ["Bearer " <> BC.pack (apiKey opts)] -- | Produce a request URL to a table.@@ -71,28 +74,21 @@ -- * API -fromResp :: (HasCallStack, FromJSON a) => Response ByteString -> a-fromResp r = decoder $ r ^. responseBody- where- decoder b = case eitherDecode b of- Left e -> error e- Right r -> r- -- | 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)+ getRecordsFromUrl (mkWreqOptions opts) (tableNameToUrl opts tname) --- | Retrieve the records for a table given a URL and network options.+-- | Retrieve the records for a table given a URL and network (Wreq) options. getRecordsFromUrl :: (HasCallStack, FromJSON a) => Options -> String -> IO (Table a) getRecordsFromUrl opts url = do resp <- getWith opts url- getMore (fromResp resp)+ getMore (respJsonBody resp) where getMore tbl = case tableOffset tbl of Just offset -> do resp <- getWith (opts & param "offset" .~ [offset]) url- getMore $ fromResp resp <> tbl+ getMore $ respJsonBody resp <> tbl Nothing -> pure tbl @@ -100,10 +96,10 @@ 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+ return $ respJsonBody resp where url = tableNameToUrl opts tname- netOpts = airtableOptionsToWreqOptions opts + netOpts = mkWreqOptions opts payload = toJSON a -- | Update a record on a given table, using the supplied fields ('a').@@ -113,7 +109,7 @@ customPayloadMethodWith "PATCH" netOpts url payload where url = tableNameToUrl opts tname <> "/" <> rec2str recId- netOpts = airtableOptionsToWreqOptions opts+ netOpts = mkWreqOptions opts payload = toJSON a -- | Delete a record on a table. @@ -121,6 +117,14 @@ deleteRecord opts tname recId = void $ deleteWith netOpts url where- netOpts = airtableOptionsToWreqOptions opts+ netOpts = mkWreqOptions opts url = tableNameToUrl opts tname <> "/" <> rec2str recId +-- * Low-level helpers++respJsonBody :: (HasCallStack, FromJSON a) => Response ByteString -> a+respJsonBody r = decoder $ r ^. responseBody+ where+ decoder b = case eitherDecode b of+ Left e -> error e+ Right r -> r
src/Airtable/Table.hs view
@@ -12,13 +12,11 @@ -- * RecordID , RecordID(..) , rec2str- -- * IsRecord class- , IsRecord(..)+ -- * HasRecordId class+ , HasRecordId(..) -- * Table , Table(..) , TableName- , parseRecord- , parseFields -- * Table methods , toList , exists@@ -53,6 +51,7 @@ -- | Airtable's record ID for use in indexing records newtype RecordID = RecordID Text deriving ( FromJSON+ , ToJSON , Show , Read , Eq@@ -65,22 +64,32 @@ rec2str :: RecordID -> String rec2str (RecordID rec) = T.unpack rec --- * IsRecord class+-- * HasRecordId class -- | A convenience typeclass for selecting records using RecordID-like keys.-class IsRecord a where+class HasRecordId a where toRecId :: a -> RecordID -instance IsRecord RecordID where+instance HasRecordId RecordID where toRecId = id -instance IsRecord String where+instance HasRecordId String where toRecId = RecordID . T.pack -- * Table -- | An airtable record.-data Record a = Record { recordId :: RecordID, recordObj :: a, createdTime :: UTCTime }+data Record a = Record + { recordId :: RecordID+ , recordObj :: a+ , createdTime :: UTCTime + } deriving ( Show+ , Read+ , Generic+ , Eq+ , Ord+ , Functor+ ) instance (FromJSON a) => FromJSON (Record a) where parseJSON = withObject "record object" $ \v -> do@@ -88,19 +97,26 @@ <*> v .: "fields" <*> v .: "createdTime" -instance IsRecord (Record a) where+instance (ToJSON a) => ToJSON (Record a) where+ toJSON rec = object [ "id" .= recordId rec+ , "fields" .= recordObj rec+ , "createdTime" .= createdTime rec+ ]++instance HasRecordId (Record a) where toRecId = recordId --- | Airtable's table type-data Table a = Table { tableRecords :: Map.HashMap RecordID a- , tableOffset :: Maybe Text- } deriving- ( Show- , Read- , Eq- , Generic- , Functor- )+-- | An airtable table. +data Table a = Table + { tableRecords :: Map.HashMap RecordID (Record a) -- ^ A mapping from RecordID to Record+ , tableOffset :: Maybe Text -- ^ The "offset" parameter for the table. Is used to deal with airtable's pagination.+ } deriving+ ( Show+ , Read+ , Eq+ , Generic+ , Functor+ ) -- | Synonym used in querying tables from the API. type TableName = String@@ -112,22 +128,9 @@ offset <- v .:? "offset" return $ Table parsedRecs offset where- parseRec tbl = withObject "record object" $ \v -> - do recId <- v .: "id"- fields <- v .: "fields"- obj <- parseJSON fields- return $ Map.insert recId obj tbl--parseRecord :: (UTCTime -> RecordID -> Value -> Parser a) -> Value -> Parser a-parseRecord action = withObject "record object" $ \v -> do- created <- v .: "createdTime"- recordId <- v .: "id"- fields <- v .: "fields"- action created recordId fields--parseFields :: (Value -> Parser a) -> Value -> Parser a-parseFields action (Object v) = v .: "fields" >>= action-parseFields _action invalid = typeMismatch "parseFields_" invalid+ parseRec tbl v = do + rec <- parseJSON v+ return $ Map.insert (recordId rec) rec tbl instance Monoid (Table a) where mempty = Table mempty Nothing@@ -136,16 +139,16 @@ -- * Table methods -- | Convert a 'Table' to a list of key-record pairs.-toList :: Table a -> [(RecordID, a)]+toList :: Table a -> [(RecordID, Record a)] toList = Map.toList . tableRecords -- | Check if a record exists at the given key in a table.-exists :: (IsRecord r) => Table a -> r -> Bool+exists :: (HasRecordId r) => Table a -> r -> Bool 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 :: (HasCallStack, HasRecordId r, Show a) => Table a -> r -> Record a select tbl rec = tableRecords tbl `lookup` toRecId rec where lookup mp k = case Map.lookup k mp of@@ -153,11 +156,11 @@ Nothing -> error $ "lookup failed in map: " <> show k -- | Safely lookup a record using its RecordID.-selectMaybe :: (IsRecord r) => Table a -> r -> Maybe a+selectMaybe :: (HasRecordId r) => Table a -> r -> Maybe (Record a) selectMaybe tbl rec = toRecId rec `Map.lookup` tableRecords tbl -- | Read all records.-selectAll :: Table a -> [a]+selectAll :: Table a -> [Record a] selectAll = map snd . toList -- | Read all RecordID's.@@ -165,13 +168,13 @@ selectAllKeys = map fst . toList -- | Select all records satisfying a condition.-selectWhere :: Table a -> (RecordID -> a -> Bool) -> [a]-selectWhere tbl f = map snd $ filter (uncurry f) (toList tbl)+selectWhere :: Table a -> (Record a -> Bool) -> [Record a]+selectWhere tbl f = filter f (selectAll tbl) -- | Select all RecordID's satisfying a condition.-selectKeyWhere :: Table a -> (RecordID -> a -> Bool) -> [RecordID]-selectKeyWhere tbl f = map fst $ filter (uncurry f) (toList tbl)+selectKeyWhere :: Table a -> (Record a -> Bool) -> [RecordID]+selectKeyWhere tbl f = map recordId $ filter f (selectAll tbl) -- | Delete all Records satisfying a condition.-deleteWhere :: Table a -> (RecordID -> a -> Bool) -> Table a-deleteWhere (Table recs off) f = Table (Map.filterWithKey (\k v -> not $ f k v) recs) off+deleteWhere :: Table a -> (Record a -> Bool) -> Table a+deleteWhere (Table recs off) f = Table (Map.filter (\v -> not $ f v) recs) off