@@ -0,0 +1,465 @@
1+module Yoga.DynamoDB.DynamoDB where
5+import Data.Maybe (Maybe(..))
6+import Data.Newtype (class Newtype)
7+import Data.Nullable (Nullable, toMaybe, toNullable)
9+import Effect.Aff (Aff)
10+import Effect.Uncurried (EffectFn1, EffectFn2, runEffectFn1, runEffectFn2)
11+import Foreign (Foreign, unsafeToForeign)
12+import Foreign.Object (Object)
13+import Foreign.Object as Object
14+import Prim.Row (class Union)
15+import Promise (Promise)
16+import Promise.Aff (toAffE) as Promise
17+import Unsafe.Coerce (unsafeCoerce)
19+-- Opaque DynamoDB types
20+foreign import data DynamoDB :: Type
21+foreign import data DynamoDBItem :: Type
23+-- Newtypes for type safety
25+-- Connection configuration
26+newtype Region = Region String
28+derive instance Newtype Region _
29+derive newtype instance Eq Region
30+derive newtype instance Show Region
32+newtype Endpoint = Endpoint String
34+derive instance Newtype Endpoint _
35+derive newtype instance Eq Endpoint
36+derive newtype instance Show Endpoint
38+newtype AccessKeyId = AccessKeyId String
40+derive instance Newtype AccessKeyId _
41+derive newtype instance Eq AccessKeyId
42+derive newtype instance Show AccessKeyId
44+newtype SecretAccessKey = SecretAccessKey String
46+derive instance Newtype SecretAccessKey _
47+derive newtype instance Eq SecretAccessKey
48+derive newtype instance Show SecretAccessKey
50+newtype SessionToken = SessionToken String
52+derive instance Newtype SessionToken _
53+derive newtype instance Eq SessionToken
54+derive newtype instance Show SessionToken
56+-- Table and item types
57+newtype TableName = TableName String
59+derive instance Newtype TableName _
60+derive newtype instance Eq TableName
61+derive newtype instance Show TableName
63+newtype IndexName = IndexName String
65+derive instance Newtype IndexName _
66+derive newtype instance Eq IndexName
67+derive newtype instance Show IndexName
69+newtype AttributeName = AttributeName String
71+derive instance Newtype AttributeName _
72+derive newtype instance Eq AttributeName
73+derive newtype instance Show AttributeName
75+newtype AttributeValue = AttributeValue Foreign
77+derive instance Newtype AttributeValue _
80+newtype KeyConditionExpression = KeyConditionExpression String
82+derive instance Newtype KeyConditionExpression _
83+derive newtype instance Eq KeyConditionExpression
84+derive newtype instance Show KeyConditionExpression
86+newtype FilterExpression = FilterExpression String
88+derive instance Newtype FilterExpression _
89+derive newtype instance Eq FilterExpression
90+derive newtype instance Show FilterExpression
92+newtype ProjectionExpression = ProjectionExpression String
94+derive instance Newtype ProjectionExpression _
95+derive newtype instance Eq ProjectionExpression
96+derive newtype instance Show ProjectionExpression
98+newtype ConditionExpression = ConditionExpression String
100+derive instance Newtype ConditionExpression _
101+derive newtype instance Eq ConditionExpression
102+derive newtype instance Show ConditionExpression
104+newtype UpdateExpression = UpdateExpression String
106+derive instance Newtype UpdateExpression _
107+derive newtype instance Eq UpdateExpression
108+derive newtype instance Show UpdateExpression
111+newtype Limit = Limit Int
113+derive instance Newtype Limit _
114+derive newtype instance Eq Limit
115+derive newtype instance Ord Limit
116+derive newtype instance Show Limit
118+-- Type-safe item conversion
119+toItem :: Foreign -> DynamoDBItem
120+toItem = unsafeCoerce
122+fromItem :: DynamoDBItem -> Foreign
123+fromItem = unsafeCoerce
125+-- Expression attribute values/names
126+type ExpressionAttributeValues = Object Foreign
127+type ExpressionAttributeNames = Object String
129+-- Helper functions for building expression attributes
130+attrNames :: forall r. Record r -> ExpressionAttributeNames
131+attrNames = unsafeCoerce
133+attrValues :: forall r. Record r -> ExpressionAttributeValues
134+attrValues rec = unsafeCoerce $ map unsafeToForeign (unsafeCoerce rec :: Object _)
136+-- Single attribute helpers
137+attrName :: String -> String -> ExpressionAttributeNames
138+attrName placeholder name = Object.singleton placeholder name
140+attrValue :: forall a. String -> a -> ExpressionAttributeValues
141+attrValue placeholder value = Object.singleton placeholder (unsafeToForeign value)
143+-- Create DynamoDB client
144+type DynamoDBConfigImpl =
146+ , endpoint :: Endpoint
148+ { accessKeyId :: AccessKeyId
149+ , secretAccessKey :: SecretAccessKey
150+ , sessionToken :: SessionToken
152+ , maxAttempts :: Int
153+ , requestTimeout :: Int
156+foreign import createDynamoDBImpl :: forall opts. EffectFn1 { | opts } DynamoDB
158+createDynamoDB :: forall opts opts_. Union opts opts_ DynamoDBConfigImpl => { | opts } -> Effect DynamoDB
159+createDynamoDB opts = runEffectFn1 createDynamoDBImpl opts
162+type PutItemOptionsImpl =
163+ ( conditionExpression :: ConditionExpression
164+ , expressionAttributeValues :: ExpressionAttributeValues
165+ , expressionAttributeNames :: ExpressionAttributeNames
166+ , returnValues :: String
170+ { attributes :: Maybe DynamoDBItem
173+foreign import putItemImpl :: forall opts. EffectFn2 DynamoDB { tableName :: TableName, item :: DynamoDBItem | opts } (Promise { attributes :: Nullable DynamoDBItem })
175+putItem :: TableName -> DynamoDBItem -> DynamoDB -> Aff PutItemResult
176+putItem tableName item db = do
177+ result <- runEffectFn2 putItemImpl db { tableName, item } # Promise.toAffE
178+ pure { attributes: toMaybe result.attributes }
180+putItemWithOptions :: forall opts opts_. Union opts opts_ PutItemOptionsImpl => TableName -> DynamoDBItem -> { | opts } -> DynamoDB -> Aff PutItemResult
181+putItemWithOptions tableName item opts db = do
182+ result <- runEffectFn2 putItemImpl db { tableName, item, conditionExpression: unsafeCoerce opts, expressionAttributeValues: unsafeCoerce opts, expressionAttributeNames: unsafeCoerce opts, returnValues: unsafeCoerce opts } # Promise.toAffE
183+ pure { attributes: toMaybe result.attributes }
186+type GetItemOptionsImpl =
187+ ( consistentRead :: Boolean
188+ , projectionExpression :: ProjectionExpression
189+ , expressionAttributeNames :: ExpressionAttributeNames
192+foreign import getItemImpl :: forall opts. EffectFn2 DynamoDB { tableName :: TableName, key :: DynamoDBItem | opts } (Promise { item :: Nullable DynamoDBItem })
194+getItem :: TableName -> DynamoDBItem -> DynamoDB -> Aff (Maybe DynamoDBItem)
195+getItem tableName key db = do
196+ result <- runEffectFn2 getItemImpl db { tableName, key } # Promise.toAffE
197+ pure $ toMaybe result.item
199+getItemWithOptions :: forall opts opts_. Union opts opts_ GetItemOptionsImpl => TableName -> DynamoDBItem -> { | opts } -> DynamoDB -> Aff (Maybe DynamoDBItem)
200+getItemWithOptions tableName key opts db = do
201+ result <- runEffectFn2 getItemImpl db { tableName, key, consistentRead: unsafeCoerce opts, projectionExpression: unsafeCoerce opts, expressionAttributeNames: unsafeCoerce opts } # Promise.toAffE
202+ pure $ toMaybe result.item
205+type UpdateItemOptionsImpl =
206+ ( conditionExpression :: ConditionExpression
207+ , expressionAttributeValues :: ExpressionAttributeValues
208+ , expressionAttributeNames :: ExpressionAttributeNames
209+ , returnValues :: String
212+type UpdateItemResult =
213+ { attributes :: Maybe DynamoDBItem
216+foreign import updateItemImpl :: forall opts. EffectFn2 DynamoDB { tableName :: TableName, key :: DynamoDBItem, updateExpression :: UpdateExpression | opts } (Promise { attributes :: Nullable DynamoDBItem })
218+updateItem :: TableName -> DynamoDBItem -> UpdateExpression -> DynamoDB -> Aff UpdateItemResult
219+updateItem tableName key updateExpr db = do
220+ result <- runEffectFn2 updateItemImpl db { tableName, key, updateExpression: updateExpr } # Promise.toAffE
221+ pure { attributes: toMaybe result.attributes }
223+updateItemWithOptions :: forall opts opts_. Union opts opts_ UpdateItemOptionsImpl => TableName -> DynamoDBItem -> UpdateExpression -> { | opts } -> DynamoDB -> Aff UpdateItemResult
224+updateItemWithOptions tableName key updateExpr opts db = do
225+ result <- runEffectFn2 updateItemImpl db { tableName, key, updateExpression: updateExpr, conditionExpression: unsafeCoerce opts, expressionAttributeValues: unsafeCoerce opts, expressionAttributeNames: unsafeCoerce opts, returnValues: unsafeCoerce opts } # Promise.toAffE
226+ pure { attributes: toMaybe result.attributes }
229+type DeleteItemOptionsImpl =
230+ ( conditionExpression :: ConditionExpression
231+ , expressionAttributeValues :: ExpressionAttributeValues
232+ , expressionAttributeNames :: ExpressionAttributeNames
233+ , returnValues :: String
236+type DeleteItemResult =
237+ { attributes :: Maybe DynamoDBItem
240+foreign import deleteItemImpl :: forall opts. EffectFn2 DynamoDB { tableName :: TableName, key :: DynamoDBItem | opts } (Promise { attributes :: Nullable DynamoDBItem })
242+deleteItem :: TableName -> DynamoDBItem -> DynamoDB -> Aff DeleteItemResult
243+deleteItem tableName key db = do
244+ result <- runEffectFn2 deleteItemImpl db { tableName, key } # Promise.toAffE
245+ pure { attributes: toMaybe result.attributes }
247+deleteItemWithOptions :: forall opts opts_. Union opts opts_ DeleteItemOptionsImpl => TableName -> DynamoDBItem -> { | opts } -> DynamoDB -> Aff DeleteItemResult
248+deleteItemWithOptions tableName key opts db = do
249+ result <- runEffectFn2 deleteItemImpl db { tableName, key, conditionExpression: unsafeCoerce opts, expressionAttributeValues: unsafeCoerce opts, expressionAttributeNames: unsafeCoerce opts, returnValues: unsafeCoerce opts } # Promise.toAffE
250+ pure { attributes: toMaybe result.attributes }
253+type QueryOptionsImpl =
254+ ( indexName :: IndexName
255+ , filterExpression :: FilterExpression
256+ , projectionExpression :: ProjectionExpression
257+ , expressionAttributeValues :: ExpressionAttributeValues
258+ , expressionAttributeNames :: ExpressionAttributeNames
259+ , consistentRead :: Boolean
260+ , scanIndexForward :: Boolean
262+ , exclusiveStartKey :: DynamoDBItem
266+ { items :: Array DynamoDBItem
268+ , scannedCount :: Int
269+ , lastEvaluatedKey :: Maybe DynamoDBItem
272+foreign import queryImpl :: forall opts. EffectFn2 DynamoDB { tableName :: TableName, keyConditionExpression :: KeyConditionExpression | opts } (Promise { items :: Array DynamoDBItem, count :: Int, scannedCount :: Int, lastEvaluatedKey :: Nullable DynamoDBItem })
274+query :: TableName -> KeyConditionExpression -> DynamoDB -> Aff QueryResult
275+query tableName keyCondition db = do
276+ result <- runEffectFn2 queryImpl db { tableName, keyConditionExpression: keyCondition } # Promise.toAffE
278+ { items: result.items
279+ , count: result.count
280+ , scannedCount: result.scannedCount
281+ , lastEvaluatedKey: toMaybe result.lastEvaluatedKey
284+queryWithOptions :: forall opts opts_. Union opts opts_ QueryOptionsImpl => TableName -> KeyConditionExpression -> { | opts } -> DynamoDB -> Aff QueryResult
285+queryWithOptions tableName keyCondition opts db = do
286+ result <- runEffectFn2 queryImpl db { tableName, keyConditionExpression: keyCondition, indexName: unsafeCoerce opts, filterExpression: unsafeCoerce opts, projectionExpression: unsafeCoerce opts, expressionAttributeValues: unsafeCoerce opts, expressionAttributeNames: unsafeCoerce opts, consistentRead: unsafeCoerce opts, scanIndexForward: unsafeCoerce opts, limit: unsafeCoerce opts, exclusiveStartKey: unsafeCoerce opts } # Promise.toAffE
288+ { items: result.items
289+ , count: result.count
290+ , scannedCount: result.scannedCount
291+ , lastEvaluatedKey: toMaybe result.lastEvaluatedKey
295+type ScanOptionsImpl =
296+ ( indexName :: IndexName
297+ , filterExpression :: FilterExpression
298+ , projectionExpression :: ProjectionExpression
299+ , expressionAttributeValues :: ExpressionAttributeValues
300+ , expressionAttributeNames :: ExpressionAttributeNames
301+ , consistentRead :: Boolean
303+ , exclusiveStartKey :: DynamoDBItem
307+ { items :: Array DynamoDBItem
309+ , scannedCount :: Int
310+ , lastEvaluatedKey :: Maybe DynamoDBItem
313+foreign import scanImpl :: forall opts. EffectFn2 DynamoDB { tableName :: TableName | opts } (Promise { items :: Array DynamoDBItem, count :: Int, scannedCount :: Int, lastEvaluatedKey :: Nullable DynamoDBItem })
315+scan :: TableName -> DynamoDB -> Aff ScanResult
316+scan tableName db = do
317+ result <- runEffectFn2 scanImpl db { tableName } # Promise.toAffE
319+ { items: result.items
320+ , count: result.count
321+ , scannedCount: result.scannedCount
322+ , lastEvaluatedKey: toMaybe result.lastEvaluatedKey
325+scanWithOptions :: forall opts opts_. Union opts opts_ ScanOptionsImpl => TableName -> { | opts } -> DynamoDB -> Aff ScanResult
326+scanWithOptions tableName opts db = do
327+ result <- runEffectFn2 scanImpl db { tableName, indexName: unsafeCoerce opts, filterExpression: unsafeCoerce opts, projectionExpression: unsafeCoerce opts, expressionAttributeValues: unsafeCoerce opts, expressionAttributeNames: unsafeCoerce opts, consistentRead: unsafeCoerce opts, limit: unsafeCoerce opts, exclusiveStartKey: unsafeCoerce opts } # Promise.toAffE
329+ { items: result.items
330+ , count: result.count
331+ , scannedCount: result.scannedCount
332+ , lastEvaluatedKey: toMaybe result.lastEvaluatedKey
335+-- Batch operations types
336+type BatchGetRequestItems = Object { keys :: Array DynamoDBItem, projectionExpression :: Maybe ProjectionExpression }
337+type BatchGetResponses = Object (Array DynamoDBItem)
339+type BatchGetResult =
340+ { responses :: BatchGetResponses
341+ , unprocessedKeys :: Object (Array DynamoDBItem)
344+foreign import batchGetItemImpl :: EffectFn2 DynamoDB Foreign (Promise { responses :: Foreign, unprocessedKeys :: Foreign })
346+batchGetItem :: Foreign -> DynamoDB -> Aff BatchGetResult
347+batchGetItem requestItems db = do
348+ result <- runEffectFn2 batchGetItemImpl db requestItems # Promise.toAffE
350+ { responses: unsafeCoerce result.responses
351+ , unprocessedKeys: unsafeCoerce result.unprocessedKeys
355+type PutRequest = { item :: DynamoDBItem }
356+type DeleteRequest = { key :: DynamoDBItem }
357+type WriteRequest = { putRequest :: Maybe PutRequest, deleteRequest :: Maybe DeleteRequest }
359+type BatchWriteResult =
360+ { unprocessedItems :: Object (Array WriteRequest)
363+foreign import batchWriteItemImpl :: EffectFn2 DynamoDB Foreign (Promise { unprocessedItems :: Foreign })
365+batchWriteItem :: Foreign -> DynamoDB -> Aff BatchWriteResult
366+batchWriteItem requestItems db = do
367+ result <- runEffectFn2 batchWriteItemImpl db requestItems # Promise.toAffE
368+ pure { unprocessedItems: unsafeCoerce result.unprocessedItems }
371+type TransactGetItem = { tableName :: TableName, key :: DynamoDBItem }
372+type TransactPutItem = { tableName :: TableName, item :: DynamoDBItem, conditionExpression :: Maybe ConditionExpression }
373+type TransactUpdateItem = { tableName :: TableName, key :: DynamoDBItem, updateExpression :: UpdateExpression }
374+type TransactDeleteItem = { tableName :: TableName, key :: DynamoDBItem, conditionExpression :: Maybe ConditionExpression }
376+type TransactGetResult =
377+ { responses :: Array { item :: Maybe DynamoDBItem }
380+foreign import transactGetItemsImpl :: EffectFn2 DynamoDB (Array Foreign) (Promise { responses :: Array { item :: Nullable DynamoDBItem } })
382+transactGetItems :: Array Foreign -> DynamoDB -> Aff TransactGetResult
383+transactGetItems items db = do
384+ result <- runEffectFn2 transactGetItemsImpl db items # Promise.toAffE
385+ pure { responses: map (\r -> { item: toMaybe r.item }) result.responses }
387+foreign import transactWriteItemsImpl :: EffectFn2 DynamoDB (Array Foreign) (Promise Unit)
389+transactWriteItems :: Array Foreign -> DynamoDB -> Aff Unit
390+transactWriteItems items db = runEffectFn2 transactWriteItemsImpl db items # Promise.toAffE
394+-- Table schema types
395+type AttributeDefinition = { attributeName :: String, attributeType :: String }
396+type KeySchemaElement = { attributeName :: String, keyType :: String }
398+type ProvisionedThroughput =
399+ { readCapacityUnits :: Int
400+ , writeCapacityUnits :: Int
403+type GlobalSecondaryIndex =
404+ { indexName :: IndexName
405+ , keySchema :: Array KeySchemaElement
406+ , projection :: { projectionType :: String }
407+ , provisionedThroughput :: Maybe ProvisionedThroughput
410+type LocalSecondaryIndex =
411+ { indexName :: IndexName
412+ , keySchema :: Array KeySchemaElement
413+ , projection :: { projectionType :: String }
416+type StreamSpecification =
417+ { streamEnabled :: Boolean
418+ , streamViewType :: String
421+type Tag = { key :: String, value :: String }
423+type CreateTableOptionsImpl =
424+ ( billingMode :: String
425+ , provisionedThroughput :: ProvisionedThroughput
426+ , globalSecondaryIndexes :: Array GlobalSecondaryIndex
427+ , localSecondaryIndexes :: Array LocalSecondaryIndex
428+ , streamSpecification :: StreamSpecification
432+foreign import createTableImpl :: forall opts. EffectFn2 DynamoDB { tableName :: TableName, attributeDefinitions :: Array AttributeDefinition, keySchema :: Array KeySchemaElement | opts } (Promise Foreign)
434+createTable :: forall opts opts_. Union opts opts_ CreateTableOptionsImpl => { tableName :: TableName, attributeDefinitions :: Array AttributeDefinition, keySchema :: Array KeySchemaElement | opts } -> DynamoDB -> Aff Foreign
435+createTable input db = runEffectFn2 createTableImpl db input # Promise.toAffE
438+foreign import describeTableImpl :: EffectFn2 DynamoDB TableName (Promise Foreign)
440+describeTable :: TableName -> DynamoDB -> Aff Foreign
441+describeTable tableName db = runEffectFn2 describeTableImpl db tableName # Promise.toAffE
444+foreign import deleteTableImpl :: EffectFn2 DynamoDB TableName (Promise Foreign)
446+deleteTable :: TableName -> DynamoDB -> Aff Foreign
447+deleteTable tableName db = runEffectFn2 deleteTableImpl db tableName # Promise.toAffE
450+type ListTablesResult =
451+ { tableNames :: Array TableName
452+ , lastEvaluatedTableName :: Maybe TableName
455+foreign import listTablesImpl :: EffectFn2 DynamoDB (Nullable Int) (Promise { tableNames :: Array TableName, lastEvaluatedTableName :: Nullable TableName })
457+listTables :: DynamoDB -> Aff ListTablesResult
459+ result <- runEffectFn2 listTablesImpl db (toNullable Nothing) # Promise.toAffE
460+ pure { tableNames: result.tableNames, lastEvaluatedTableName: toMaybe result.lastEvaluatedTableName }
462+listTablesLimit :: Int -> DynamoDB -> Aff ListTablesResult
463+listTablesLimit limit db = do
464+ result <- runEffectFn2 listTablesImpl db (toNullable $ Just limit) # Promise.toAffE
465+ pure { tableNames: result.tableNames, lastEvaluatedTableName: toMaybe result.lastEvaluatedTableName }