diff --git a/README.md b/README.md index 3380319..8cf5064 100644 --- a/README.md +++ b/README.md @@ -17,26 +17,27 @@ Table items can be represented using F# records: open FSharp.AWS.DynamoDB type WorkItemInfo = - { - [] - ProcessId : int64 - [] - WorkItemId : int64 - - Name : string - UUID : Guid - Dependencies : Set - Started : DateTimeOffset option - } + { + [] + ProcessId : int64 + [] + WorkItemId : int64 + + Name : string + UUID : Guid + Dependencies : Set + Started : DateTimeOffset option + } ``` We can now perform table operations on DynamoDB like so: ```fsharp open Amazon.DynamoDBv2 +open FSharp.AWS.DynamoDB.Scripting // Expose non-Async methods, e.g. PutItem/GetItem let client : IAmazonDynamoDB = ``your DynamoDB client instance`` -let table = TableContext.Create(client, tableName = "workItems", createIfNotExists = true) +let table = TableContext.Initialize(client, tableName = "workItems", Throughput.OnDemand) let workItem = { ProcessId = 0L ; WorkItemId = 1L ; Name = "Test" ; UUID = guid() ; Dependencies = set ["mscorlib"] ; Started = None } @@ -48,7 +49,7 @@ Queries and scans can be performed using quoted predicates: ```fsharp let qResults = table.Query(keyCondition = <@ fun r -> r.ProcessId = 0 @>, - filterCondition = <@ fun r -> r.Name = "test" @>) + filterCondition = <@ fun r -> r.Name = "test" @>) let sResults = table.Scan <@ fun r -> r.Started.Value >= DateTimeOffset.Now - TimeSpan.FromMinutes 1. @> ``` @@ -57,10 +58,10 @@ Values can be updated using quoted update expressions: ```fsharp let updated = table.UpdateItem(<@ fun r -> { r with Started = Some DateTimeOffset.Now } @>, - preCondition = <@ fun r -> r.DateTimeOffset = None @>) + preCondition = <@ fun r -> r.DateTimeOffset = None @>) ``` -Or they can be updated using the `UpdateOp` DSL, +Or they can be updated using [the `SET`, `ADD`, `REMOVE` and `DELETE` operations of the UpdateOp` DSL](./src/FSharp.AWS.DynamoDB/Types.fs#263), which is closer to the underlying DynamoDB API: ```fsharp @@ -99,24 +100,35 @@ Update expressions support the following F# value constructors: * `Option.Value` and `Option.get`. * `fst` and `snd` for tuple records. -## Example: Creating an atomic counter +## Example: Representing an atomic counter as an Item in a DynamoDB Table ```fsharp type private CounterEntry = { [] Id : Guid ; Value : int64 } type Counter private (table : TableContext, key : TableKey) = - member _.Value = table.GetItem(key).Value - member _.Incr() = - let updated = table.UpdateItem(key, <@ fun e -> { e with Value = e.Value + 1L } @>) - updated.Value - - static member Create(client : IAmazonDynamoDB, table : string) = - let table = TableContext.Create(client, table, createIfNotExists = true) - let entry = { Id = Guid.NewGuid() ; Value = 0L } - let key = table.PutItem entry - new Counter(table, key) + + member _.Value = async { + let! current = table.GetItemAsync(key) + return current.Value + } + + member _.Incr() = async { + let! updated = table.UpdateItemAsync(key, <@ fun e -> { e with Value = e.Value + 1L } @>) + return updated.Value + } + + static member Create(client : IAmazonDynamoDB, tableName : string) = async { + let table = TableContext(client, tableName) + let throughput = ProvisionedThroughput(readCapacityUnits = 10L, writeCapacityUnits = 10L) + do! table.VerifyOrCreateTableAsync(Throughput.Provisioned throughput) + let initialEntry = { Id = Guid.NewGuid() ; Value = 0L } + let! key = table.PutItemAsync(initialEntry) + return Counter(table, key) + } ``` +_NOTE: It's advised to split single time initialization/verification of table creation from the application logic, see [`Script.fsx`](src/FSharp.AWS.DynamoDB/Script.fsx#99) for further details_. + ## Projection Expressions Projection expressions can be used to fetch a subset of table attributes, which can be useful when performing large queries: @@ -125,7 +137,7 @@ Projection expressions can be used to fetch a subset of table attributes, which table.QueryProjected(<@ fun r -> r.HashKey = "Foo" @>, <@ fun r -> r.HashKey, r.Values.Nested.[0] @>) ``` -which returns a tuple of the specified attributes. Tuples can be of any arity and must contain non-conflicting document paths. +the resulting value is a tuple of the specified attributes. Tuples can be of any arity but must contain non-conflicting document paths. ## Secondary Indices @@ -141,7 +153,8 @@ type Record = ``` Queries can now be performed on the `GSIH` and `GSIR` fields as if they were regular hashkey and rangekey attributes. -Global secondary indices are created using the same provisioned throughput as the primary keys. + +_NOTE: Global secondary indices are created using the same provisioned throughput as for the primary keys_. [Local Secondary Indices](http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/LSI.html) can be defined using the `LocalSecondaryIndex` attribute: ```fsharp @@ -212,13 +225,13 @@ It is possible to precompute a DynamoDB expression as follows: let precomputedConditional = table.Template.PrecomputeConditionalExpr <@ fun w -> w.Name <> "test" && w.Dependencies.Contains "mscorlib" @> ``` -This precomputed conditional can now be used in place of the original expression in the FSharp.AWS.DynamoDB API: +This precomputed conditional can now be used in place of the original expression in the `FSharp.AWS.DynamoDB` API: ```fsharp let results = table.Scan precomputedConditional ``` -FSharp.AWS.DynamoDB also supports precomputation of parametric expressions: +`FSharp.AWS.DynamoDB` also supports precomputation of parametric expressions: ```fsharp let startedBefore = table.Template.PrecomputeConditionalExpr <@ fun time w -> w.StartTime.Value <= time @> @@ -232,16 +245,15 @@ table.Scan(startedBefore (DateTimeOffset.Now - TimeSpan.FromDays 1.)) A hook is provided so metrics can be published via your preferred Observability provider. For example, using [Prometheus.NET](https://github.com/prometheus-net/prometheus-net): ```fsharp -let dbCounter = Metrics.CreateCounter ("aws_dynamodb_requests_total", "Count of all DynamoDB requests", "table", "operation") +let dbCounter = Prometheus.Metrics.CreateCounter("aws_dynamodb_requests_total", "Count of all DynamoDB requests", "table", "operation") let processMetrics (m : RequestMetrics) = - dbCounter.WithLabels(m.TableName, string m.Operation).Inc () |> ignore -let table = TableContext.Create(client, tableName = "workItems", metricsCollector = processMetrics) + dbCounter.WithLabels(m.TableName, string m.Operation).Inc() +let table = TableContext(client, tableName = "workItems", metricsCollector = processMetrics) ``` -If `metricsCollector` is supplied, the requests will include `ReturnConsumedCapacity = ReturnConsumedCapacity.INDEX` +If `metricsCollector` is supplied, the requests will set `ReturnConsumedCapacity` to `ReturnConsumedCapacity.INDEX` and the `RequestMetrics` parameter will contain a list of `ConsumedCapacity` objects returned from the DynamoDB operations. - ### Building & Running Tests To build using the dotnet SDK: diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index c130ea4..53a0807 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,3 +1,16 @@ +### 0.10.0-beta +* Added `TableContext` constructor (replaces `TableContext.Create(verifyTable = false)`) +* Added `TableContext.VerifyOrCreateTableAsync` (replaces `TableContext.VerifyTableAsync(createIfNotExists = true)`) +* Added `TableContext.UpdateTableIfRequiredAsync` (conditional `UpdateTableAsync` to establish specified `throughput` or `streaming` only if required. Replaces `UpdateProvisionedThroughputAsync`) +* Added `TableContext.Scripting.Initialize` (two overloads, replacing `TableContext.Create()` and `TableContext.Create(createIfNotExists = true)`) +* Added `Throughput.OnDemand` mode (sets `BillingMode` to `PAY_PER_REQUEST`, to go with the existing support for configuring `PROVISIONED` and a `ProvisionedThroughput`) +* Added ability to configure DynamoDB streaming (via a `Streaming` DU) to `VerifyOrCreateTableAsync` and `UpdateTableIfRequiredAsync` +* Obsoleted `TableContext.Create` (replace with `TableContext.Scripting.Initialize`, `TableContext.VerifyOrCreateTableAsync`, `TableContext.VerifyTableAsync`) +* Obsoleted `TableContext.UpdateProvisionedThroughputAsync` (replace with `TableContext.UpdateTableIfRequiredAsync`) +* (breaking) Obsoleted `TableContext.VerifyTableAsync` optional argument to create a Table (replace with `VerifyOrCreateTableAsync`) +* (breaking) Changed `TableKeySchemata.CreateCreateTableRequest` to `ApplyToCreateTableRequest` (with minor signature change) +* (breaking) Removed `TableContext.CreateAsync` (replace with `TableContext.VerifyTableAsync` or `VerifyOrCreateTableAsync`) + ### 0.9.4-beta * Moved Sync-over-Async versions of `TableContext` operations into `namespace FSharp.AWS.DynamoDB.Scripting` * Added `WithMetricsCollector()` copy method to allow separating metrics by context (eg by request) diff --git a/src/FSharp.AWS.DynamoDB/FSharp.AWS.DynamoDB.fsproj b/src/FSharp.AWS.DynamoDB/FSharp.AWS.DynamoDB.fsproj index 0aa6e6c..f391456 100644 --- a/src/FSharp.AWS.DynamoDB/FSharp.AWS.DynamoDB.fsproj +++ b/src/FSharp.AWS.DynamoDB/FSharp.AWS.DynamoDB.fsproj @@ -9,6 +9,7 @@ Copyright 2016 + MIT https://github.com/fsprojects/FSharp.AWS.DynamoDB/blob/master/License.md https://github.com/fsprojects/FSharp.AWS.DynamoDB https://avatars0.githubusercontent.com/u/6001315 @@ -44,4 +45,4 @@ - \ No newline at end of file + diff --git a/src/FSharp.AWS.DynamoDB/RecordKeySchema.fs b/src/FSharp.AWS.DynamoDB/RecordKeySchema.fs index 434166f..81f089d 100644 --- a/src/FSharp.AWS.DynamoDB/RecordKeySchema.fs +++ b/src/FSharp.AWS.DynamoDB/RecordKeySchema.fs @@ -368,13 +368,10 @@ type TableKeySchemata with yield! td.GlobalSecondaryIndexes |> Seq.map mkGlobalSecondaryIndex yield! td.LocalSecondaryIndexes |> Seq.map mkLocalSecondaryIndex |]) - /// Create a CreateTableRequest using supplied key schema - member schema.CreateCreateTableRequest (tableName : string, provisionedThroughput : ProvisionedThroughput) = - let ctr = CreateTableRequest(TableName = tableName) + /// Applies the settings implied by the schema to the supplied CreateTableRequest + member schema.ApplyToCreateTableRequest(ctr : CreateTableRequest) = let inline mkKSE n t = KeySchemaElement(n, t) - ctr.ProvisionedThroughput <- provisionedThroughput - let keyAttrs = new Dictionary() for tks in schema.Schemata do keyAttrs.[tks.HashKey.AttributeName] <- tks.HashKey @@ -391,7 +388,6 @@ type TableKeySchemata with gsi.KeySchema.Add <| mkKSE tks.HashKey.AttributeName KeyType.HASH tks.RangeKey |> Option.iter (fun rk -> gsi.KeySchema.Add <| mkKSE rk.AttributeName KeyType.RANGE) gsi.Projection <- Projection(ProjectionType = ProjectionType.ALL) - gsi.ProvisionedThroughput <- provisionedThroughput ctr.GlobalSecondaryIndexes.Add gsi | LocalSecondaryIndex name -> @@ -405,5 +401,3 @@ type TableKeySchemata with for attr in keyAttrs.Values do let ad = AttributeDefinition(attr.AttributeName, attr.KeyType) ctr.AttributeDefinitions.Add ad - - ctr diff --git a/src/FSharp.AWS.DynamoDB/Script.fsx b/src/FSharp.AWS.DynamoDB/Script.fsx index 10c835b..38b9417 100644 --- a/src/FSharp.AWS.DynamoDB/Script.fsx +++ b/src/FSharp.AWS.DynamoDB/Script.fsx @@ -1,7 +1,7 @@ #if USE_PUBLISHED_NUGET // If you don't want to do a local build first #r "nuget: FSharp.AWS.DynamoDB, *-*" // *-* to white-list the fact that all releases to date have been `-beta` sufficed #else -#I "../../bin/net6.0/" +#I "../../tests/FSharp.AWS.DynamoDB.Tests/bin/Debug/net6.0/" #r "AWSSDK.Core.dll" #r "AWSSDK.DynamoDBv2.dll" #r "FSharp.AWS.DynamoDB.dll" @@ -15,19 +15,18 @@ open FSharp.AWS.DynamoDB open FSharp.AWS.DynamoDB.Scripting // non-Async overloads #if USE_CLOUD -open Amazon -open Amazon.Util -let account = AWSCredentialsProfile.LoadFrom("default").Credentials -let ddb = new AmazonDynamoDBClient(account, RegionEndpoint.EUCentral1) :> IAmazonDynamoDB +open Amazon.DynamoDBv2 +let ok, creds = Amazon.Runtime.CredentialManagement.CredentialProfileStoreChain().TryGetAWSCredentials("default") +let ddb = if ok then new AmazonDynamoDBClient(creds) :> IAmazonDynamoDB else failwith "Unable to load default credentials" #else // Use Docker-hosted dynamodb-local instance // See https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DynamoDBLocal.DownloadingAndRunning.html#docker for details of how to deploy a simulator instance -let clientConfig = AmazonDynamoDBConfig(ServiceURL = "http://localhost:8000") #if USE_CREDS_FROM_ENV_VARS // 'AWS_ACCESS_KEY_ID' and 'AWS_SECRET_ACCESS_KEY' must be set for this to work let credentials = AWSCredentials.FromEnvironmentVariables() #else // Credentials are not validated if connecting to local instance so anything will do (this avoids it looking for profiles to be configured) let credentials = Amazon.Runtime.BasicAWSCredentials("A", "A") #endif +let clientConfig = AmazonDynamoDBConfig(ServiceURL = "http://localhost:8000") let ddb = new AmazonDynamoDBClient(credentials, clientConfig) :> IAmazonDynamoDB #endif @@ -54,8 +53,8 @@ type Test = Bytes : byte[] } - -let table = TableContext.Create(ddb, "test", createIfNotExists = true) +let throughput = ProvisionedThroughput(readCapacityUnits = 10L, writeCapacityUnits = 10L) +let table = TableContext.Initialize(ddb, "test", Throughput.Provisioned throughput) let value = { HashKey = Guid.NewGuid() ; List = [] ; RangeKey = "2" ; Value = 3.1415926 ; Date = DateTimeOffset.Now + TimeSpan.FromDays 2. ; Value2 = None ; Values = [|{ A = "foo" ; B = System.Reflection.BindingFlags.Instance }|] ; Map = Map.ofList [("A1",1)] ; Set = [set [1L];set [2L]] ; Bytes = [|1uy..10uy|]; String = ref "1a" ; Unions = [A 42; B("42",3)]} @@ -94,3 +93,104 @@ let uexpr2 = table.Template.PrecomputeUpdateExpr <@ fun v r -> { r with Value2 = for i = 1 to 1000 do let _ = table.UpdateItem(key, uexpr2 (Some 42)) () + +(* Expanded version of README sample that illustrates how one can better split Table initialization from application logic *) + +type internal CounterEntry = { [] Id : Guid ; Value : int64 } + +/// Represents a single Item in a Counters Table +type Counter internal (table : TableContext, key : TableKey) = + + static member internal Start(table : TableContext) = async { + let initialEntry = { Id = Guid.NewGuid() ; Value = 0L } + let! key = table.PutItemAsync(initialEntry) + return Counter(table, key) + } + + member _.Value = async { + let! current = table.GetItemAsync(key) + return current.Value + } + + member _.Incr() = async { + let! updated = table.UpdateItemAsync(key, <@ fun (e : CounterEntry) -> { e with Value = e.Value + 1L } @>) + return updated.Value + } + +/// Wrapper that creates/verifies the table only once per call to Create() +/// This does assume that your application will be sufficiently privileged to create tables on the fly +type EasyCounters private (table : TableContext) = + + // We only want to do the initialization bit once per instance of our application + static member Create(client : IAmazonDynamoDB, tableName : string) : Async = async { + let table = TableContext(client, tableName) + // Create the table if necessary. Verifies schema is correct if it has already been created + // NOTE the hard coded initial throughput provisioning - arguably this belongs outside of your application logic + let throughput = ProvisionedThroughput(readCapacityUnits = 10L, writeCapacityUnits = 10L) + do! table.VerifyOrCreateTableAsync(Throughput.Provisioned throughput) + return EasyCounters(table) + } + + member _.StartCounter() : Async = + Counter.Start table + +/// Variant of EasyCounters that splits the provisioning step from the (optional) validation that the table is present +type SimpleCounters private (table : TableContext) = + + static member Provision(client : IAmazonDynamoDB, tableName : string, readCapacityUnits, writeCapacityUnits) : Async = async { + let table = TableContext(client, tableName) + let provisionedThroughput = ProvisionedThroughput(readCapacityUnits, writeCapacityUnits) + let throughput = Throughput.Provisioned provisionedThroughput + // normally, RCU/WCU provisioning only happens first time the Table is created and is then considered an external concern + // here we use `UpdateTableIfRequiredAsync` to reset it each time we deploy the app + do! table.VerifyOrCreateTableAsync(throughput) + do! table.UpdateTableIfRequiredAsync(throughput) } + + static member ProvisionOnDemand(client : IAmazonDynamoDB, tableName : string) : Async = async { + let table = TableContext(client, tableName) + let throughput = Throughput.OnDemand + do! table.VerifyOrCreateTableAsync(throughput) + // as per the Provision, above, we reset to OnDemand, if it got reconfigured since it was originally created + do! table.UpdateTableIfRequiredAsync(throughput) } + + /// We only want to do the initialization bit once per instance of our application + /// Similar to EasyCounters.Create in that it ensures the table is provisioned correctly + /// However it will never actually create the table + static member CreateWithVerify(client : IAmazonDynamoDB, tableName : string) : Async = async { + let table = TableContext(client, tableName) + // This validates the Table has been created correctly + // (in general this is a good idea, but it is an optional step so it can be skipped, i.e. see Create() below) + do! table.VerifyTableAsync() + return SimpleCounters(table) + } + + /// Assumes the table has been provisioned externally via Provision() + static member Create(client : IAmazonDynamoDB, tableName : string) : SimpleCounters = + // NOTE we are skipping + SimpleCounters(TableContext(client, tableName)) + + member _.StartCounter() : Async = + Counter.Start table + +let e = EasyCounters.Create(ddb, "testing") |> Async.RunSynchronously +let e1 = e.StartCounter() |> Async.RunSynchronously +let e2 = e.StartCounter() |> Async.RunSynchronously +e1.Incr() |> Async.RunSynchronously +e2.Incr() |> Async.RunSynchronously + +// First, we create it in On-Demand mode +SimpleCounters.ProvisionOnDemand(ddb, "testing-pre-provisioned") |> Async.RunSynchronously +// Then we flip it to Provisioned mode +SimpleCounters.Provision(ddb, "testing-pre-provisioned", readCapacityUnits = 10L, writeCapacityUnits = 10L) |> Async.RunSynchronously +// The consuming code can assume the provisioning has been carried out as part of the deploy +// that allows the creation to be synchronous (and not impede application startup) +let s = SimpleCounters.Create(ddb, "testing-pre-provisioned") +let s1 = s.StartCounter() |> Async.RunSynchronously // Throws if Provision step has not been executed +s1.Incr() |> Async.RunSynchronously + +// Alternately, we can have the app do an extra call (and have some asynchronous initialization work) to check the table is ready +let v = SimpleCounters.CreateWithVerify(ddb, "testing-not-present") |> Async.RunSynchronously // Throws, as table not present +let v2 = v.StartCounter() |> Async.RunSynchronously +v2.Incr() |> Async.RunSynchronously + +// (TOCONSIDER: Illustrate how to use AsyncCacheCell from https://github.com/jet/equinox/blob/master/src/Equinox.Core/AsyncCacheCell.fs to make Verify call lazy) diff --git a/src/FSharp.AWS.DynamoDB/TableContext.fs b/src/FSharp.AWS.DynamoDB/TableContext.fs index d946a5b..aa521f2 100644 --- a/src/FSharp.AWS.DynamoDB/TableContext.fs +++ b/src/FSharp.AWS.DynamoDB/TableContext.fs @@ -8,7 +8,6 @@ open Microsoft.FSharp.Quotations open Amazon.DynamoDBv2 open Amazon.DynamoDBv2.Model -open FSharp.AWS.DynamoDB.KeySchema open FSharp.AWS.DynamoDB.ExprCommon /// Exception raised by DynamoDB in case where write preconditions are not satisfied @@ -17,9 +16,172 @@ type ConditionalCheckFailedException = Amazon.DynamoDBv2.Model.ConditionalCheckF /// Exception raised by DynamoDB in case where resources are not found type ResourceNotFoundException = Amazon.DynamoDBv2.Model.ResourceNotFoundException -/// Represents the provisioned throughput for given table or index +/// Represents the provisioned throughput for a Table or Global Secondary Index type ProvisionedThroughput = Amazon.DynamoDBv2.Model.ProvisionedThroughput +/// Represents the throughput configuration for a Table +[] +type Throughput = + | Provisioned of ProvisionedThroughput + | OnDemand +module internal Throughput = + let applyToCreateRequest (req : CreateTableRequest) = function + | Throughput.Provisioned t -> + req.BillingMode <- BillingMode.PROVISIONED + req.ProvisionedThroughput <- t + for gsi in req.GlobalSecondaryIndexes do + gsi.ProvisionedThroughput <- t + | Throughput.OnDemand -> + req.BillingMode <- BillingMode.PAY_PER_REQUEST + let requiresUpdate (desc : TableDescription) = function + | Throughput.Provisioned t -> + let current = desc.ProvisionedThroughput + match desc.BillingModeSummary with + | null -> false // can happen if initial create did not explicitly specify a BillingMode when creating + | bms -> bms.BillingMode <> BillingMode.PROVISIONED + || t.ReadCapacityUnits <> current.ReadCapacityUnits + || t.WriteCapacityUnits <> current.WriteCapacityUnits + | Throughput.OnDemand -> + match desc.BillingModeSummary with + | null -> true // CreateTable without setting BillingMode is equivalent to it being BullingMode.PROVISIONED + | bms -> bms.BillingMode <> BillingMode.PAY_PER_REQUEST + let applyToUpdateRequest (req : UpdateTableRequest) = function + | Throughput.Provisioned t -> + req.BillingMode <- BillingMode.PROVISIONED + req.ProvisionedThroughput <- t + | Throughput.OnDemand -> + req.BillingMode <- BillingMode.PAY_PER_REQUEST + +/// Represents the streaming configuration for a Table +[] +type Streaming = + | Enabled of StreamViewType + | Disabled +module internal Streaming = + let private (|Spec|) = function + | Streaming.Enabled svt -> StreamSpecification(StreamEnabled = true, StreamViewType = svt) + | Streaming.Disabled -> StreamSpecification(StreamEnabled = false) + let applyToCreateRequest (req : CreateTableRequest) (Spec spec) = + req.StreamSpecification <- spec + let requiresUpdate (desc : TableDescription) = function + | Streaming.Disabled -> desc.StreamSpecification.StreamEnabled + | Streaming.Enabled svt -> not desc.StreamSpecification.StreamEnabled || desc.StreamSpecification.StreamViewType <> svt + let applyToUpdateRequest (req : UpdateTableRequest) (Spec spec) = + req.StreamSpecification <- spec + +module internal CreateTableRequest = + + let create (tableName, template : RecordTemplate<'TRecord>) throughput streaming customize = + let req = CreateTableRequest(TableName = tableName) + template.Info.Schemata.ApplyToCreateTableRequest req // NOTE needs to precede the throughput application as that walks the GSIs list + throughput |> Option.iter (Throughput.applyToCreateRequest req) // NOTE needs to succeed Schemata.ApplyToCreateTableRequest + streaming |> Option.iter (Streaming.applyToCreateRequest req) + customize |> Option.iter (fun c -> c req) + req + + let execute (client : IAmazonDynamoDB) request : Async = async { + let! ct = Async.CancellationToken + return! client.CreateTableAsync(request, ct) |> Async.AwaitTaskCorrect + } + +module internal UpdateTableRequest = + + let create tableName = + UpdateTableRequest(TableName = tableName) + + let apply throughput streaming request = + throughput |> Option.iter (Throughput.applyToUpdateRequest request) + streaming |> Option.iter (Streaming.applyToUpdateRequest request) + + // Yields a request only if throughput, streaming or customize determine the update is warranted + let createIfRequired tableName tableDescription throughput streaming customize : UpdateTableRequest option= + let request = create tableName + let tc = throughput |> Option.filter (Throughput.requiresUpdate tableDescription) + let sc = streaming |> Option.filter (Streaming.requiresUpdate tableDescription) + match tc, sc, customize with + | None, None, None -> None + | Some _ as tc, sc, None + | tc, (Some _ as sc), None -> + apply tc sc request + Some request + | tc, sc, Some customize -> + apply tc sc request + if customize request then Some request + else None + + let execute (client : IAmazonDynamoDB) request : Async = async { + let! ct = Async.CancellationToken + let! _response = client.UpdateTableAsync(request, ct) |> Async.AwaitTaskCorrect in () + } + +module internal Provisioning = + + let tryDescribe (client : IAmazonDynamoDB, tableName : string) : Async = async { + let! ct = Async.CancellationToken + let! td = client.DescribeTableAsync(tableName, ct) |> Async.AwaitTaskCorrect + return match td.Table with t when t.TableStatus = TableStatus.ACTIVE -> Some t | _ -> None + } + + let private waitForActive (client : IAmazonDynamoDB, tableName : string) : Async = + let rec wait () = async { + match! tryDescribe (client, tableName) with + | Some t -> return t + | None -> + do! Async.Sleep 1000 + // wait indefinitely if table is in transition state + return! wait () + } + wait () + + let (|Conflict|_|) (e : exn) = + match e with + | :? AmazonDynamoDBException as e when e.StatusCode = HttpStatusCode.Conflict -> Some() + | :? ResourceInUseException -> Some () + | _ -> None + + let private checkOrCreate (client, tableName) validateDescription maybeMakeCreateTableRequest : Async = + let rec aux retries = async { + match! waitForActive (client, tableName) |> Async.Catch with + | Choice1Of2 desc -> + validateDescription desc + return desc + + | Choice2Of2 (:? ResourceNotFoundException) when Option.isSome maybeMakeCreateTableRequest -> + let req = maybeMakeCreateTableRequest.Value () + match! CreateTableRequest.execute client req |> Async.Catch with + | Choice1Of2 _ -> return! aux retries + | Choice2Of2 Conflict when retries > 0 -> + do! Async.Sleep 2000 + return! aux (retries - 1) + + | Choice2Of2 e -> return! Async.Raise e + + | Choice2Of2 Conflict when retries > 0 -> + do! Async.Sleep 2000 + return! aux (retries - 1) + + | Choice2Of2 e -> return! Async.Raise e + } + aux 9 // up to 9 retries, i.e. 10 attempts before we let exception propagate + + let private validateDescription (tableName, template : RecordTemplate<'TRecord>) desc = + let existingSchema = TableKeySchemata.OfTableDescription desc + if existingSchema <> template.Info.Schemata then + sprintf "table '%s' exists with key schema %A, which is incompatible with record '%O'." + tableName existingSchema typeof<'TRecord> + |> invalidOp + + let private run (client, tableName, template) maybeMakeCreateRequest : Async = + let validate = validateDescription (tableName, template) + checkOrCreate (client, tableName) validate maybeMakeCreateRequest |> Async.Ignore + + let verifyOrCreate (client, tableName, template) throughput streaming customize : Async = + let generateCreateRequest () = CreateTableRequest.create (tableName, template) throughput streaming customize + run (client, tableName, template) (Some generateCreateRequest) + + let validateOnly (client, tableName, template) : Async = + run (client, tableName, template) None + /// Represents the operation performed on the table, for metrics collection purposes type Operation = GetItem | PutItem | UpdateItem | DeleteItem | BatchGetItems | BatchWriteItems | Scan | Query @@ -260,6 +422,19 @@ type TableContext<'TRecord> internal /// Record-induced table template member __.Template = template + + /// + /// Creates a DynamoDB client instance for given F# record and table name.
+ /// For creating, provisioning or verification, see VerifyOrCreateTableAsync and VerifyTableAsync. + ///
+ /// DynamoDB client instance. + /// Table name to target. + /// Function to receive request metrics. + new (client : IAmazonDynamoDB, tableName : string, ?metricsCollector : RequestMetrics -> unit) = + if not <| isValidTableName tableName then invalidArg "tableName" "unsupported DynamoDB table name." + TableContext<'TRecord>(client, tableName, RecordTemplate.Define<'TRecord>(), metricsCollector) + + /// Creates a new table context instance that uses /// a new F# record type. The new F# record type /// must define a compatible key schema. @@ -846,88 +1021,66 @@ type TableContext<'TRecord> internal /// - /// Asynchronously updates the underlying table with supplied provisioned throughput. + /// Asynchronously verifies that the table exists and is compatible with record key schema, throwing if it is incompatible.
+ /// If the table is not present, it is created, with the specified throughput (and optionally streaming) configuration.
+ /// See also VerifyTableAsync, which only verifies the Table is present and correct.
+ /// See also UpdateTableIfRequiredAsync, which will adjust throughput and streaming if they are not as specified. ///
- /// Provisioned throughput to use on table. - member __.UpdateProvisionedThroughputAsync(provisionedThroughput : ProvisionedThroughput) : Async = async { - let request = UpdateTableRequest(tableName, provisionedThroughput) - let! ct = Async.CancellationToken - let! _response = client.UpdateTableAsync(request, ct) |> Async.AwaitTaskCorrect - return () - } - + /// Throughput configuration to use for the table. + /// Optional streaming configuration to apply for the table. Default: Disabled.. + /// Callback to post-process the CreateTableRequest. + member t.VerifyOrCreateTableAsync(throughput : Throughput, ?streaming, ?customize) : Async = + Provisioning.verifyOrCreate (client, tableName, template) (Some throughput) streaming customize /// - /// Asynchronously verify that the table exists and is compatible with record key schema. + /// Asynchronously verify that the table exists and is compatible with record key schema, or throw.
+ /// See also VerifyOrCreateTableAsync, which performs the same check, but can create or re-provision the Table if required. ///
- /// Create the table instance now instance if it does not exist. Defaults to false. - /// Provisioned throughput for the table if newly created. Defaults to (10,10). - member __.VerifyTableAsync(?createIfNotExists : bool, ?provisionedThroughput : ProvisionedThroughput) : Async = async { - let createIfNotExists = defaultArg createIfNotExists false - let (|Conflict|_|) (e : exn) = - match e with - | :? AmazonDynamoDBException as e when e.StatusCode = HttpStatusCode.Conflict -> Some() - | :? ResourceInUseException -> Some () - | _ -> None - - let rec verify lastExn retries = async { - match lastExn with - | Some e when retries = 0 -> do! Async.Raise e - | _ -> () - - let! ct = Async.CancellationToken - let! response = - client.DescribeTableAsync(tableName, ct) - |> Async.AwaitTaskCorrect - |> Async.Catch - - match response with - | Choice1Of2 td -> - if td.Table.TableStatus <> TableStatus.ACTIVE then - do! Async.Sleep 2000 - // wait indefinitely if table is in transition state - return! verify None retries - else - - let existingSchema = TableKeySchemata.OfTableDescription td.Table - if existingSchema <> template.Info.Schemata then - sprintf "table '%s' exists with key schema %A, which is incompatible with record '%O'." - tableName existingSchema typeof<'TRecord> - |> invalidOp - - | Choice2Of2 (:? ResourceNotFoundException) when createIfNotExists -> - let provisionedThroughput = - match provisionedThroughput with - | None -> ProvisionedThroughput(10L,10L) - | Some pt -> pt - - let ctr = template.Info.Schemata.CreateCreateTableRequest (tableName, provisionedThroughput) - let! ct = Async.CancellationToken - let! response = - client.CreateTableAsync(ctr, ct) - |> Async.AwaitTaskCorrect - |> Async.Catch - - match response with - | Choice1Of2 _ -> return! verify None retries - | Choice2Of2 (Conflict as e) -> - do! Async.Sleep 2000 - return! verify (Some e) (retries - 1) - - | Choice2Of2 e -> do! Async.Raise e - - | Choice2Of2 (Conflict as e) -> - do! Async.Sleep 2000 - return! verify (Some e) (retries - 1) + member _.VerifyTableAsync() : Async = + Provisioning.validateOnly (client, tableName, template) - | Choice2Of2 e -> do! Async.Raise e - } + /// + /// Adjusts the Table's configuration via UpdateTable if the throughput or streaming are not as specified.
+ /// NOTE: The underlying API can throw if a change is currently in progress; see the DynamoDB UpdateTable API documentation.
+ /// NOTE: Throws InvalidOperationException if the table is not yet Active. It is recommended to ensure the Table is prepared via VerifyTableAsync or VerifyOrCreateTableAsync to guard against the potential for this state. + ///
+ /// Throughput configuration to use for the table. Always applied via either CreateTable or UpdateTable. + /// Optional streaming configuration to apply for the table. Default (if creating): Disabled. Default: (if existing) do not change. + /// Callback to post-process the UpdateTableRequest. UpdateTable is inhibited if it returns false and no other configuration requires a change. + /// Current table configuration, if known. Retrieved via DescribeTable if not supplied. + member t.UpdateTableIfRequiredAsync(?throughput : Throughput, ?streaming, ?custom, ?currentTableDescription : TableDescription) : Async = async { + let! tableDescription = async { + match currentTableDescription with + | Some d -> return d + | None -> + match! Provisioning.tryDescribe (client, tableName) with + | Some d -> return d + | None -> return invalidOp "Table is not currently Active. Please use VerifyTableAsync or VerifyOrCreateTableAsync to guard against this state." } + match UpdateTableRequest.createIfRequired tableName tableDescription throughput streaming custom with + | None -> () + | Some request -> do! UpdateTableRequest.execute client request } - do! verify None 10 - } + /// Asynchronously updates the underlying table with supplied provisioned throughput. + /// Provisioned throughput to use on table. + [] + member t.UpdateProvisionedThroughputAsync(provisionedThroughput : ProvisionedThroughput) : Async = + t.UpdateTableIfRequiredAsync(Throughput.Provisioned provisionedThroughput) -/// Table context factory methods -type TableContext = + /// Asynchronously verify that the table exists and is compatible with record key schema. + /// Create the table instance now instance if it does not exist. Defaults to false. + /// Provisioned throughput for the table if newly created. Defaults to (10,10). + [] + member t.VerifyTableAsync(?createIfNotExists : bool, ?provisionedThroughput : ProvisionedThroughput) : Async = + if createIfNotExists = Some true then + let throughput = match provisionedThroughput with Some p -> p | None -> ProvisionedThroughput(10L, 10L) + t.VerifyOrCreateTableAsync(Throughput.Provisioned throughput) + else + t.VerifyTableAsync() + +// Deprecated factory method, to be removed. Replaced with +// 1. TableContext<'T> ctor (synchronous) +// 2. VerifyOrCreateTableAsync OR VerifyTableAsync (explicitly async to signify that verification/creation is a costly and/or privileged operation) +type TableContext internal () = /// /// Creates a DynamoDB client instance for given F# record and table name. @@ -935,21 +1088,23 @@ type TableContext = /// DynamoDB client instance. /// Table name to target. /// Verify that the table exists and is compatible with supplied record schema. Defaults to true. - /// Create the table now instance if it does not exist. Defaults to false. - /// Provisioned throughput for the table if newly created. Defaults to (10,10). + /// Create the table now if it does not exist. Defaults to false. + /// Provisioned throughput for the table if newly created. Default: 10 RCU, 10 WCU /// Function to receive request metrics. - static member CreateAsync<'TRecord>(client : IAmazonDynamoDB, tableName : string, ?verifyTable : bool, ?createIfNotExists : bool, - ?provisionedThroughput : ProvisionedThroughput, ?metricsCollector : RequestMetrics -> unit) : Async> = async { - - if not <| isValidTableName tableName then invalidArg "tableName" "unsupported DynamoDB table name." - let verifyTable = defaultArg verifyTable true - let createIfNotExists = defaultArg createIfNotExists false - let context = new TableContext<'TRecord>(client, tableName, RecordTemplate.Define<'TRecord>(), metricsCollector) - if verifyTable || createIfNotExists then - do! context.VerifyTableAsync(createIfNotExists = createIfNotExists, ?provisionedThroughput = provisionedThroughput) - - return context - } + [] + static member Create<'TRecord> + ( client : IAmazonDynamoDB, tableName : string, ?verifyTable : bool, + ?createIfNotExists : bool, ?provisionedThroughput : ProvisionedThroughput, + ?metricsCollector : RequestMetrics -> unit) = async { + let context = TableContext<'TRecord>(client, tableName, ?metricsCollector = metricsCollector) + if createIfNotExists = Some true then + let throughput = match provisionedThroughput with Some p -> p | None -> ProvisionedThroughput(10L, 10L) + do! context.VerifyOrCreateTableAsync(Throughput.Provisioned throughput) + elif verifyTable <> Some false then + do! context.VerifyTableAsync() + return context } /// /// Sync-over-Async helpers that can be opted-into when working in scripting scenarios. @@ -958,6 +1113,32 @@ type TableContext = /// module Scripting = + /// Factory methods for scripting scenarios + type TableContext internal () = + + /// + /// Creates a DynamoDB client instance for the specified F# record type, client and table name.
+ /// Validates the table exists, and has the correct schema as per VerifyTableAsync.
+ /// See other overload for VerifyOrCreateTableAsync semantics. + ///
+ /// DynamoDB client instance. + /// Table name to target. + static member Initialize<'TRecord>(client : IAmazonDynamoDB, tableName : string) : TableContext<'TRecord> = + let context = TableContext<'TRecord>(client, tableName) + context.VerifyTableAsync() |> Async.RunSynchronously + context + + /// Creates a DynamoDB client instance for the specified F# record type, client and table name.
+ /// Either validates the table exists and has the correct schema, or creates a fresh one, as per VerifyOrCreateTableAsync.
+ /// See other overload for VerifyTableAsync semantics. + /// DynamoDB client instance. + /// Table name to target. + /// Throughput to configure if the Table does not yet exist. + static member Initialize<'TRecord>(client : IAmazonDynamoDB, tableName : string, throughput) : TableContext<'TRecord> = + let context = TableContext<'TRecord>(client, tableName) + context.VerifyOrCreateTableAsync(throughput) |> Async.RunSynchronously + context + type TableContext<'TRecord> with /// @@ -1369,36 +1550,8 @@ module Scripting = |> Async.RunSynchronously - /// - /// Updates the underlying table with supplied provisioned throughput. - /// + /// Updates the underlying table with supplied provisioned throughput. /// Provisioned throughput to use on table. - member __.UpdateProvisionedThroughput(provisionedThroughput : ProvisionedThroughput) = - __.UpdateProvisionedThroughputAsync(provisionedThroughput) |> Async.RunSynchronously - - - /// - /// Asynchronously verify that the table exists and is compatible with record key schema. - /// - /// Create the table instance now if it does not exist. Defaults to false. - /// Provisioned throughput for the table if newly created. - member __.VerifyTable(?createIfNotExists : bool, ?provisionedThroughput : ProvisionedThroughput) = - __.VerifyTableAsync(?createIfNotExists = createIfNotExists, ?provisionedThroughput = provisionedThroughput) - |> Async.RunSynchronously - - type TableContext with - - /// - /// Creates a DynamoDB client instance for given F# record and table name. - /// - /// DynamoDB client instance. - /// Table name to target. - /// Verify that the table exists and is compatible with supplied record schema. Defaults to true. - /// Create the table now instance if it does not exist. Defaults to false. - /// Provisioned throughput for the table if newly created. - /// Function to receive request metrics. - static member Create<'TRecord>(client : IAmazonDynamoDB, tableName : string, ?verifyTable : bool, ?createIfNotExists : bool, - ?provisionedThroughput : ProvisionedThroughput, ?metricsCollector : RequestMetrics -> unit) = - TableContext.CreateAsync<'TRecord>(client, tableName, ?verifyTable = verifyTable, ?createIfNotExists = createIfNotExists, - ?provisionedThroughput = provisionedThroughput, ?metricsCollector = metricsCollector) - |> Async.RunSynchronously + member t.UpdateProvisionedThroughput(provisionedThroughput : ProvisionedThroughput) = + let spec = Throughput.Provisioned provisionedThroughput + t.UpdateTableIfRequiredAsync(spec) |> Async.RunSynchronously diff --git a/tests/FSharp.AWS.DynamoDB.Tests/ConditionalExpressionTests.fs b/tests/FSharp.AWS.DynamoDB.Tests/ConditionalExpressionTests.fs index 894569f..13ae79a 100644 --- a/tests/FSharp.AWS.DynamoDB.Tests/ConditionalExpressionTests.fs +++ b/tests/FSharp.AWS.DynamoDB.Tests/ConditionalExpressionTests.fs @@ -86,7 +86,7 @@ type ``Conditional Expression Tests`` (fixture : TableFixture) = Serialized = rand(), guid() } - let table = TableContext.Create(fixture.Client, fixture.TableName, createIfNotExists = true) + let table = fixture.CreateEmpty() let [] ``Item exists precondition`` () = let item = mkItem() diff --git a/tests/FSharp.AWS.DynamoDB.Tests/MetricsCollectorTests.fs b/tests/FSharp.AWS.DynamoDB.Tests/MetricsCollectorTests.fs index d720074..5bf6ef7 100644 --- a/tests/FSharp.AWS.DynamoDB.Tests/MetricsCollectorTests.fs +++ b/tests/FSharp.AWS.DynamoDB.Tests/MetricsCollectorTests.fs @@ -52,7 +52,7 @@ let (|TotalCu|) : ConsumedCapacity list -> float = Seq.sumBy (fun c -> c.Capacit /// Tests without common setup type Tests(fixture : TableFixture) = - let rawTable = TableContext.Create(fixture.Client, fixture.TableName, createIfNotExists = true) + let rawTable = fixture.CreateEmpty() let (|ExpectedTableName|_|) name = if name = fixture.TableName then Some () else None @@ -84,7 +84,7 @@ type Tests(fixture : TableFixture) = /// Tests that look up a specific item. Each test run gets a fresh individual item type ItemTests(fixture : TableFixture) = - let rawTable = TableContext.Create(fixture.Client, fixture.TableName, createIfNotExists = true) + let rawTable = fixture.CreateEmpty() let (|ExpectedTableName|_|) name = if name = fixture.TableName then Some () else None let item = mkItem (guid()) (guid()) 0 @@ -128,10 +128,10 @@ type ItemTests(fixture : TableFixture) = interface IClassFixture -/// Heavy tests reliant on establishing (and mutating) multiple items. Separate Test Class so Xunit will run thme in parallel with others +/// Heavy tests reliant on establishing (and mutating) multiple items. Separate Test Class so Xunit will run them in parallel with others type BulkMutationTests(fixture : TableFixture) = - let rawTable = TableContext.Create(fixture.Client, fixture.TableName, createIfNotExists = true) + let rawTable = fixture.CreateEmpty() let (|ExpectedTableName|_|) name = if name = fixture.TableName then Some () else None // NOTE we mutate the items so they need to be established each time @@ -169,7 +169,7 @@ type ManyReadOnlyItemsFixture() = inherit TableFixture() // TOCONSIDER shift this into IAsyncLifetime.InitializeAsync - let table = TableContext.Create(base.Client, base.TableName, createIfNotExists = true) + let table = base.CreateEmpty() let hk = guid () do let gsk = guid () diff --git a/tests/FSharp.AWS.DynamoDB.Tests/PaginationTests.fs b/tests/FSharp.AWS.DynamoDB.Tests/PaginationTests.fs index a470d07..93d6833 100644 --- a/tests/FSharp.AWS.DynamoDB.Tests/PaginationTests.fs +++ b/tests/FSharp.AWS.DynamoDB.Tests/PaginationTests.fs @@ -43,7 +43,7 @@ type ``Pagination Tests`` (fixture : TableFixture) = LocalAttribute = int (rand () % 2L) } - let table = TableContext.Create(fixture.Client, fixture.TableName, createIfNotExists = true) + let table = fixture.CreateEmpty() let [] ``Paginated Query on Primary Key`` () = let hk = guid() diff --git a/tests/FSharp.AWS.DynamoDB.Tests/ProjectionExpressionTests.fs b/tests/FSharp.AWS.DynamoDB.Tests/ProjectionExpressionTests.fs index 115bc78..5b039cd 100644 --- a/tests/FSharp.AWS.DynamoDB.Tests/ProjectionExpressionTests.fs +++ b/tests/FSharp.AWS.DynamoDB.Tests/ProjectionExpressionTests.fs @@ -97,7 +97,7 @@ type ``Projection Expression Tests`` (fixture : TableFixture) = Serialized = rand(), guid() ; Serialized2 = { NV = guid() ; NE = enum (int (rand()) % 3) } ; } - let table = TableContext.Create(fixture.Client, fixture.TableName, createIfNotExists = true) + let table = fixture.CreateEmpty() let [] ``Should fail on invalid projections`` () = let testProj (p : Expr 'T>) = diff --git a/tests/FSharp.AWS.DynamoDB.Tests/SimpleTableOperationTests.fs b/tests/FSharp.AWS.DynamoDB.Tests/SimpleTableOperationTests.fs index 5ed0710..ec671a9 100644 --- a/tests/FSharp.AWS.DynamoDB.Tests/SimpleTableOperationTests.fs +++ b/tests/FSharp.AWS.DynamoDB.Tests/SimpleTableOperationTests.fs @@ -56,7 +56,7 @@ type ``Simple Table Operation Tests`` (fixture : TableFixture) = Unions = [Choice1Of3 (guid()) ; Choice2Of3(rand()) ; Choice3Of3(Guid.NewGuid().ToByteArray())] } - let table = TableContext.Create(fixture.Client, fixture.TableName, createIfNotExists = true) + let table = fixture.CreateEmpty() let [] ``Convert to compatible table`` () = let table' = table.WithRecordType () diff --git a/tests/FSharp.AWS.DynamoDB.Tests/SparseGSITests.fs b/tests/FSharp.AWS.DynamoDB.Tests/SparseGSITests.fs index 9a68be7..70e9bda 100644 --- a/tests/FSharp.AWS.DynamoDB.Tests/SparseGSITests.fs +++ b/tests/FSharp.AWS.DynamoDB.Tests/SparseGSITests.fs @@ -31,7 +31,7 @@ type ``Sparse GSI Tests`` (fixture : TableFixture) = SecondaryHashKey = if rand() % 2L = 0L then Some (guid()) else None ; } - let table = TableContext.Create(fixture.Client, fixture.TableName, createIfNotExists = true) + let table = fixture.CreateEmpty() let [] ``GSI Put Operation`` () = let value = mkItem() diff --git a/tests/FSharp.AWS.DynamoDB.Tests/UpdateExpressionTests.fs b/tests/FSharp.AWS.DynamoDB.Tests/UpdateExpressionTests.fs index ff4ff7d..f9d1343 100644 --- a/tests/FSharp.AWS.DynamoDB.Tests/UpdateExpressionTests.fs +++ b/tests/FSharp.AWS.DynamoDB.Tests/UpdateExpressionTests.fs @@ -94,7 +94,7 @@ type ``Update Expression Tests``(fixture : TableFixture) = Serialized = rand(), guid() ; Serialized2 = { NV = guid() ; NE = enum (int (rand()) % 3) } ; } - let table = TableContext.Create(fixture.Client, fixture.TableName, createIfNotExists = true) + let table = fixture.CreateEmpty() let [] ``Attempt to update HashKey`` () = let item = mkItem() diff --git a/tests/FSharp.AWS.DynamoDB.Tests/Utils.fs b/tests/FSharp.AWS.DynamoDB.Tests/Utils.fs index fc59b79..606ed0c 100644 --- a/tests/FSharp.AWS.DynamoDB.Tests/Utils.fs +++ b/tests/FSharp.AWS.DynamoDB.Tests/Utils.fs @@ -5,18 +5,20 @@ open System.IO open FsCheck open Swensen.Unquote +open Xunit + +open FSharp.AWS.DynamoDB open Amazon.DynamoDBv2 open Amazon.Runtime -open Xunit [] module Utils = - let getRandomTableName() = - sprintf "fsdynamodb-%s" <| Guid.NewGuid().ToString("N") + let guid () = Guid.NewGuid().ToString("N") - let guid() = Guid.NewGuid().ToString("N") + let getRandomTableName() = + sprintf "fsdynamodb-%s" <| guid () let shouldFailwith<'T, 'Exn when 'Exn :> exn>(f : unit -> 'T) = <@ f () |> ignore @> @@ -24,8 +26,8 @@ module Utils = let getDynamoDBAccount () = let credentials = BasicAWSCredentials("Fake", "Fake") - let config = AmazonDynamoDBConfig() - config.ServiceURL <- "http://localhost:8000" + let config = AmazonDynamoDBConfig(ServiceURL = "http://localhost:8000") + new AmazonDynamoDBClient(credentials, config) :> IAmazonDynamoDB @@ -37,14 +39,19 @@ module Utils = type TableFixture() = + let client = getDynamoDBAccount() let tableName = getRandomTableName() + member _.Client = client member _.TableName = tableName + member _.CreateEmpty<'TRecord>() = + let throughput = ProvisionedThroughput(readCapacityUnits = 10L, writeCapacityUnits = 10L) + Scripting.TableContext.Initialize<'TRecord>(client, tableName, Throughput.Provisioned throughput) + interface IAsyncLifetime with member _.InitializeAsync() = System.Threading.Tasks.Task.CompletedTask member _.DisposeAsync() = client.DeleteTableAsync(tableName) - diff --git a/tests/FSharp.AWS.DynamoDB.Tests/packages.config b/tests/FSharp.AWS.DynamoDB.Tests/packages.config deleted file mode 100644 index c899fcc..0000000 --- a/tests/FSharp.AWS.DynamoDB.Tests/packages.config +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file