github.com/aldelo/common@v1.5.1/wrapper/dynamodb/dynamodb.go (about) 1 package dynamodb 2 3 /* 4 * Copyright 2020-2023 Aldelo, LP 5 * 6 * Licensed under the Apache License, Version 2.0 (the "License"); 7 * you may not use this file except in compliance with the License. 8 * You may obtain a copy of the License at 9 * 10 * http://www.apache.org/licenses/LICENSE-2.0 11 * 12 * Unless required by applicable law or agreed to in writing, software 13 * distributed under the License is distributed on an "AS IS" BASIS, 14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 * See the License for the specific language governing permissions and 16 * limitations under the License. 17 */ 18 19 // ================================================================================================================= 20 // AWS CREDENTIAL: 21 // use $> aws configure (to set aws access key and secret to target machine) 22 // Store AWS Access ID and Secret Key into Default Profile Using '$ aws configure' cli 23 // 24 // To Install & Setup AWS CLI on Host: 25 // 1) https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2-linux.html 26 // On Ubuntu, if host does not have zip and unzip: 27 // $> sudo apt install zip 28 // $> sudo apt install unzip 29 // On Ubuntu, to install AWS CLI v2: 30 // $> curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" 31 // $> unzip awscliv2.zip 32 // $> sudo ./aws/install 33 // 2) $> aws configure set region awsRegionName --profile default 34 // 3) $> aws configure 35 // follow prompts to enter Access ID and Secret Key 36 // 37 // AWS Region Name Reference: 38 // us-west-2, us-east-1, ap-northeast-1, etc 39 // See: https://docs.aws.amazon.com/general/latest/gr/rande.html 40 // ================================================================================================================= 41 42 import ( 43 "context" 44 "errors" 45 "fmt" 46 util "github.com/aldelo/common" 47 awshttp2 "github.com/aldelo/common/wrapper/aws" 48 "github.com/aldelo/common/wrapper/aws/awsregion" 49 "github.com/aldelo/common/wrapper/xray" 50 "github.com/aws/aws-dax-go/dax" 51 "github.com/aws/aws-sdk-go/aws" 52 "github.com/aws/aws-sdk-go/aws/awserr" 53 "github.com/aws/aws-sdk-go/aws/session" 54 "github.com/aws/aws-sdk-go/service/dynamodb" 55 "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute" 56 "github.com/aws/aws-sdk-go/service/dynamodb/expression" 57 awsxray "github.com/aws/aws-xray-sdk-go/xray" 58 "log" 59 "net/http" 60 "reflect" 61 "time" 62 ) 63 64 // ================================================================================================================ 65 // STRUCTS 66 // ================================================================================================================ 67 68 // DynamoDB struct encapsulates the AWS DynamoDB access functionality 69 // 70 // Notes: 71 // 1. to use dax, must be within vpc with dax cluster subnet pointing to private ip subnet of the vpc 72 // 2. dax is not accessible outside of vpc 73 // 3. on ec2 or container within vpc, also need aws credential via aws cli too = aws configure 74 type DynamoDB struct { 75 // define the AWS region that dynamodb is serviced from 76 AwsRegion awsregion.AWSRegion 77 78 // custom http2 client options 79 HttpOptions *awshttp2.HttpClientSettings 80 81 // define the Dax Endpoint (required if using DAX) 82 DaxEndpoint string 83 84 // dynamodb connection object 85 cn *dynamodb.DynamoDB 86 87 // dax connection object 88 cnDax *dax.Dax 89 90 // if dax is enabled, skip dax will skip dax and route direct to DynamoDB 91 // if dax is not enabled, skip dax true or not will always route to DynamoDB 92 SkipDax bool 93 94 // operating table 95 TableName string 96 PKName string 97 SKName string 98 99 // last execute param string 100 LastExecuteParamsPayload string 101 102 _parentSegment *xray.XRayParentSegment 103 } 104 105 // DynamoDBError struct contains special status info including error and retry advise 106 type DynamoDBError struct { 107 ErrorMessage string 108 SuppressError bool 109 110 AllowRetry bool 111 RetryNeedsBackOff bool 112 } 113 114 // Error returns error string of the struct object 115 func (e *DynamoDBError) Error() string { 116 return e.ErrorMessage 117 } 118 119 // DynamoDBTableKeys struct defines the PK and SK fields to be used in key search (Always PK and SK) 120 // 121 // important 122 // 123 // if dynamodb table is defined as PK and SK together, then to search, MUST use PK and SK together or error will trigger 124 // 125 // ResultItemPtr = optional, used with TransactionGetItems() to denote output unmarshal object target 126 type DynamoDBTableKeys struct { 127 PK string 128 SK string 129 130 ResultItemPtr interface{} `dynamodbav:"-"` 131 ResultError error `dynamodbav:"-"` 132 133 resultProcessed bool 134 } 135 136 // DynamoDBUnprocessedItemsAndKeys defines struct to slices of items and keys 137 type DynamoDBUnprocessedItemsAndKeys struct { 138 PutItems []map[string]*dynamodb.AttributeValue 139 DeleteKeys []*DynamoDBTableKeys 140 } 141 142 // UnmarshalPutItems will convert struct's PutItems into target slice of struct objects 143 // 144 // notes: 145 // 146 // resultItemsPtr interface{} = Input is Slice of Actual Struct Objects 147 func (u *DynamoDBUnprocessedItemsAndKeys) UnmarshalPutItems(resultItemsPtr interface{}) error { 148 if u == nil { 149 return errors.New("UnmarshalPutItems Failed: (Validate) " + "DynamoDBUnprocessedItemsAndKeys Object Nil") 150 } 151 152 if resultItemsPtr == nil { 153 return errors.New("UnmarshalPutItems Failed: (Validate) " + "ResultItems Object Nil") 154 } 155 156 if err := dynamodbattribute.UnmarshalListOfMaps(u.PutItems, resultItemsPtr); err != nil { 157 return errors.New("UnmarshalPutItems Failed: (Unmarshal) " + err.Error()) 158 } else { 159 // success 160 return nil 161 } 162 } 163 164 // DynamoDBUpdateItemInput defines a single item update instruction 165 // 166 // important 167 // 168 // if dynamodb table is defined as PK and SK together, then to search, MUST use PK and SK together or error will trigger 169 // 170 // parameters: 171 // 172 // pkValue = required, value of partition key to seek 173 // skValue = optional, value of sort key to seek; set to blank if value not provided 174 // 175 // updateExpression = required, ATTRIBUTES ARE CASE SENSITIVE; set remove add or delete action expression, see Rules URL for full detail 176 // Rules: 177 // 1) https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.UpdateExpressions.html 178 // Usage Syntax: 179 // 1) Action Keywords are: set, add, remove, delete 180 // 2) Each Action Keyword May Appear in UpdateExpression Only Once 181 // 3) Each Action Keyword Grouping May Contain One or More Actions, Such as 'set price=:p, age=:age, etc' (each action separated by comma) 182 // 4) Each Action Keyword Always Begin with Action Keyword itself, such as 'set ...', 'add ...', etc 183 // 5) If Attribute is Numeric, Action Can Perform + or - Operation in Expression, such as 'set age=age-:newAge, price=price+:price, etc' 184 // 6) If Attribute is Slice, Action Can Perform Slice Element Operation in Expression, such as 'set age[2]=:newData, etc' 185 // 7) When Attribute Name is Reserved Keyword, Use ExpressionAttributeNames to Define #xyz to Alias 186 // a) Use the #xyz in the KeyConditionExpression such as #yr = :year (:year is Defined ExpressionAttributeValue) 187 // 8) When Attribute is a List, Use list_append(a, b, ...) in Expression to append elements (list_append() is case sensitive) 188 // a) set #ri = list_append(#ri, :vals) where :vals represents one or more of elements to add as in L 189 // 9) if_not_exists(path, value) 190 // a) Avoids existing attribute if already exists 191 // b) set price = if_not_exists(price, :p) 192 // c) if_not_exists is case sensitive; path is the existing attribute to check 193 // 10) Action Type Purposes 194 // a) SET = add one or more attributes to an item; overrides existing attributes in item with new values; if attribute is number, able to perform + or - operations 195 // b) REMOVE = remove one or more attributes from an item, to remove multiple attributes, separate by comma; remove element from list use xyz[1] index notation 196 // c) ADD = adds a new attribute and its values to an item; if attribute is number and already exists, value will add up or subtract 197 // d) DELETE = supports only on set data types; deletes one or more elements from a set, such as 'delete color :c' 198 // 11) Example 199 // a) set age=:age, name=:name, etc 200 // b) set age=age-:age, num=num+:num, etc 201 // 202 // conditionExpress = optional, ATTRIBUTES ARE CASE SENSITIVE; sets conditions for this condition expression, set to blank if not used 203 // Usage Syntax: 204 // 1) "size(info.actors) >= :num" 205 // a) When Length of Actors Attribute Value is Equal or Greater Than :num, ONLY THEN UpdateExpression is Performed 206 // 2) ExpressionAttributeName and ExpressionAttributeValue is Still Defined within ExpressionAttributeNames and ExpressionAttributeValues Where Applicable 207 // 208 // expressionAttributeNames = optional, ATTRIBUTES ARE CASE SENSITIVE; set nil if not used, must define for attribute names that are reserved keywords such as year, data etc. using #xyz 209 // Usage Syntax: 210 // 1) map[string]*string: where string is the #xyz, and *string is the original xyz attribute name 211 // a) map[string]*string { "#xyz": aws.String("Xyz"), } 212 // 2) Add to Map 213 // a) m := make(map[string]*string) 214 // b) m["#xyz"] = aws.String("Xyz") 215 // 216 // expressionAttributeValues = required, ATTRIBUTES ARE CASE SENSITIVE; sets the value token and value actual to be used within the keyConditionExpression; this sets both compare token and compare value 217 // Usage Syntax: 218 // 1) map[string]*dynamodb.AttributeValue: where string is the :xyz, and *dynamodb.AttributeValue is { S: aws.String("abc"), }, 219 // a) map[string]*dynamodb.AttributeValue { ":xyz" : { S: aws.String("abc"), }, ":xyy" : { N: aws.String("123"), }, } 220 // 2) Add to Map 221 // a) m := make(map[string]*dynamodb.AttributeValue) 222 // b) m[":xyz"] = &dynamodb.AttributeValue{ S: aws.String("xyz") } 223 // 3) Slice of Strings -> CONVERT To Slice of *dynamodb.AttributeValue = []string -> []*dynamodb.AttributeValue 224 // a) av, err := dynamodbattribute.MarshalList(xyzSlice) 225 // b) ExpressionAttributeValue, Use 'L' To Represent the List for av defined in 3.a above 226 type DynamoDBUpdateItemInput struct { 227 PK string 228 SK string 229 UpdateExpression string 230 ConditionExpression string 231 ExpressionAttributeNames map[string]*string 232 ExpressionAttributeValues map[string]*dynamodb.AttributeValue 233 } 234 235 // DynamoDBTransactionWrites defines one or more items to put, update or delete 236 // 237 // notes 238 // 239 // PutItems interface{} = is Slice of PutItems: []Xyz 240 // a) We use interface{} because []interface{} will require each element conversion (instead we will handle conversion by internal code) 241 // b) PutItems ALWAYS Slice of Struct (Value), NOT pointers to Structs 242 type DynamoDBTransactionWrites struct { 243 PutItems interface{} 244 UpdateItems []*DynamoDBUpdateItemInput 245 DeleteItems []*DynamoDBTableKeys 246 TableNameOverride string 247 } 248 249 // MarshalPutItems will marshal dynamodb transaction writes' put items into []map[string]*dynamodb.AttributeValue 250 func (w *DynamoDBTransactionWrites) MarshalPutItems() (result []map[string]*dynamodb.AttributeValue, err error) { 251 if w == nil { 252 return nil, errors.New("MarshalPutItems Failed: (Validate) " + "DynamoDBTransactionWrites Object Nil") 253 } 254 255 // validate 256 if w.PutItems == nil { 257 // no PutItems 258 return nil, nil 259 } 260 261 // get []interface{} 262 itemsIf := util.SliceObjectsToSliceInterface(w.PutItems) 263 264 if itemsIf == nil { 265 // no PutItems 266 return nil, errors.New("MarshalPutItems Failed: (Slice Convert) " + "Interface Slice Nil") 267 } 268 269 if len(itemsIf) <= 0 { 270 // no PutItems 271 return nil, nil 272 } 273 274 // loop thru each put item to marshal 275 for _, v := range itemsIf { 276 if m, e := dynamodbattribute.MarshalMap(v); e != nil { 277 return nil, errors.New("MarshalPutItems Failed: (Marshal) " + e.Error()) 278 } else { 279 if m != nil { 280 result = append(result, m) 281 } else { 282 return nil, errors.New("MarshalPutItems Failed: (Marshal) " + "Marshaled Result Nil") 283 } 284 } 285 } 286 287 // return result 288 return result, nil 289 } 290 291 // DynamoDBTransactionReads defines one or more items to get by PK / SK 292 type DynamoDBTransactionReads struct { 293 Keys []*DynamoDBTableKeys 294 TableNameOverride string 295 } 296 297 // ================================================================================================================ 298 // STRUCTS FUNCTIONS 299 // ================================================================================================================ 300 301 // ---------------------------------------------------------------------------------------------------------------- 302 // utility functions 303 // ---------------------------------------------------------------------------------------------------------------- 304 305 // handleError is an internal helper method to evaluate dynamodb error, 306 // and to advise if retry, immediate retry, suppress error etc error handling advisory 307 // 308 // notes: 309 // 310 // RetryNeedsBackOff = true indicates when doing retry, must wait an arbitrary time duration before retry; false indicates immediate is ok 311 func (d *DynamoDB) handleError(err error, errorPrefix ...string) *DynamoDBError { 312 if err != nil { 313 prefix := "" 314 315 if len(errorPrefix) > 0 { 316 prefix = errorPrefix[0] + " " 317 } 318 319 prefixType := "" 320 origError := "" 321 322 if aerr, ok := err.(awserr.Error); ok { 323 // aws errors 324 prefixType = "[AWS] " 325 326 if aerr.OrigErr() != nil { 327 origError = "OrigErr = " + aerr.OrigErr().Error() 328 } else { 329 origError = "OrigErr = Nil" 330 } 331 332 switch aerr.Code() { 333 case dynamodb.ErrCodeConditionalCheckFailedException: 334 fallthrough 335 case dynamodb.ErrCodeResourceInUseException: 336 fallthrough 337 case dynamodb.ErrCodeResourceNotFoundException: 338 fallthrough 339 case dynamodb.ErrCodeIdempotentParameterMismatchException: 340 fallthrough 341 case dynamodb.ErrCodeBackupInUseException: 342 fallthrough 343 case dynamodb.ErrCodeBackupNotFoundException: 344 fallthrough 345 case dynamodb.ErrCodeContinuousBackupsUnavailableException: 346 fallthrough 347 case dynamodb.ErrCodeGlobalTableAlreadyExistsException: 348 fallthrough 349 case dynamodb.ErrCodeGlobalTableNotFoundException: 350 fallthrough 351 case dynamodb.ErrCodeIndexNotFoundException: 352 fallthrough 353 case dynamodb.ErrCodeInvalidRestoreTimeException: 354 fallthrough 355 case dynamodb.ErrCodePointInTimeRecoveryUnavailableException: 356 fallthrough 357 case dynamodb.ErrCodeReplicaAlreadyExistsException: 358 fallthrough 359 case dynamodb.ErrCodeReplicaNotFoundException: 360 fallthrough 361 case dynamodb.ErrCodeTableAlreadyExistsException: 362 fallthrough 363 case dynamodb.ErrCodeTableInUseException: 364 fallthrough 365 case dynamodb.ErrCodeTableNotFoundException: 366 fallthrough 367 case dynamodb.ErrCodeTransactionCanceledException: 368 fallthrough 369 case dynamodb.ErrCodeTransactionConflictException: 370 fallthrough 371 case dynamodb.ErrCodeTransactionInProgressException: 372 // show error + no retry 373 return &DynamoDBError{ 374 ErrorMessage: prefix + prefixType + aerr.Code() + " - " + aerr.Message() + " - " + origError, 375 SuppressError: false, 376 AllowRetry: false, 377 RetryNeedsBackOff: false, 378 } 379 380 case dynamodb.ErrCodeItemCollectionSizeLimitExceededException: 381 fallthrough 382 case dynamodb.ErrCodeLimitExceededException: 383 // show error + allow retry with backoff 384 return &DynamoDBError{ 385 ErrorMessage: prefix + prefixType + aerr.Code() + " - " + aerr.Message() + " - " + origError, 386 SuppressError: false, 387 AllowRetry: true, 388 RetryNeedsBackOff: true, 389 } 390 391 case dynamodb.ErrCodeProvisionedThroughputExceededException: 392 fallthrough 393 case dynamodb.ErrCodeRequestLimitExceeded: 394 // no error + allow retry with backoff 395 return &DynamoDBError{ 396 ErrorMessage: prefix + prefixType + aerr.Code() + " - " + aerr.Message() + " - " + origError, 397 SuppressError: true, 398 AllowRetry: true, 399 RetryNeedsBackOff: true, 400 } 401 402 case dynamodb.ErrCodeInternalServerError: 403 // no error + allow auto retry without backoff 404 return &DynamoDBError{ 405 ErrorMessage: prefix + prefixType + aerr.Code() + " - " + aerr.Message() + " - " + origError, 406 SuppressError: true, 407 AllowRetry: true, 408 RetryNeedsBackOff: false, 409 } 410 411 default: 412 return &DynamoDBError{ 413 ErrorMessage: prefix + prefixType + aerr.Code() + " - " + aerr.Message() + " - " + origError, 414 SuppressError: false, 415 AllowRetry: false, 416 RetryNeedsBackOff: false, 417 } 418 } 419 } else { 420 // other errors 421 prefixType = "[General] " 422 423 return &DynamoDBError{ 424 ErrorMessage: prefix + prefixType + err.Error(), 425 SuppressError: false, 426 AllowRetry: false, 427 RetryNeedsBackOff: false, 428 } 429 } 430 } else { 431 // no error 432 return nil 433 } 434 } 435 436 // Connect will establish a connection to the dynamodb service 437 func (d *DynamoDB) Connect(parentSegment ...*xray.XRayParentSegment) (err error) { 438 if xray.XRayServiceOn() { 439 if len(parentSegment) > 0 { 440 d._parentSegment = parentSegment[0] 441 } 442 443 seg := xray.NewSegment("DynamoDB-Connect", d._parentSegment) 444 defer seg.Close() 445 defer func() { 446 _ = seg.Seg.AddMetadata("DynamoDB-AWS-Region", d.AwsRegion) 447 _ = seg.Seg.AddMetadata("DynamoDB-Table-Name", d.TableName) 448 449 if err != nil { 450 _ = seg.Seg.AddError(err) 451 } 452 }() 453 454 err = d.connectInternal() 455 456 if err == nil { 457 awsxray.AWS(d.cn.Client) 458 } 459 460 return err 461 } else { 462 return d.connectInternal() 463 } 464 } 465 466 // Connect will establish a connection to the dynamodb service 467 func (d *DynamoDB) connectInternal() error { 468 // clean up prior cn reference 469 d.cn = nil 470 d.SkipDax = false 471 472 if !d.AwsRegion.Valid() || d.AwsRegion == awsregion.UNKNOWN { 473 return errors.New("Connect To DynamoDB Failed: (AWS Session Error) " + "Region is Required") 474 } 475 476 // create custom http2 client if needed 477 var httpCli *http.Client 478 var httpErr error 479 480 if d.HttpOptions == nil { 481 d.HttpOptions = new(awshttp2.HttpClientSettings) 482 } 483 484 // use custom http2 client 485 h2 := &awshttp2.AwsHttp2Client{ 486 Options: d.HttpOptions, 487 } 488 489 if httpCli, httpErr = h2.NewHttp2Client(); httpErr != nil { 490 return errors.New("Connect to DynamoDB Failed: (AWS Session Error) " + "Create Custom Http2 Client Errored = " + httpErr.Error()) 491 } 492 493 // establish aws session connection and connect to dynamodb service 494 if sess, err := session.NewSession( 495 &aws.Config{ 496 Region: aws.String(d.AwsRegion.Key()), 497 HTTPClient: httpCli, 498 }); err != nil { 499 // aws session error 500 return errors.New("Connect To DynamoDB Failed: (AWS Session Error) " + err.Error()) 501 } else { 502 // aws session obtained 503 d.cn = dynamodb.New(sess) 504 505 if d.cn == nil { 506 return errors.New("Connect To DynamoDB Failed: (New DynamoDB Connection) " + "Connection Object Nil") 507 } 508 509 // successfully connected to dynamodb service 510 return nil 511 } 512 } 513 514 // EnableDax will enable dax service for this dynamodb session 515 func (d *DynamoDB) EnableDax() (err error) { 516 // get new xray segment for tracing 517 seg := xray.NewSegmentNullable("DynamoDB-EnableDax", d._parentSegment) 518 519 if seg != nil { 520 defer seg.Close() 521 defer func() { 522 _ = seg.Seg.AddMetadata("DynamoDB-Dax-Endpoint", d.DaxEndpoint) 523 524 if err != nil { 525 _ = seg.Seg.AddError(err) 526 } 527 }() 528 529 err = d.enableDaxInternal() 530 return err 531 } else { 532 return d.enableDaxInternal() 533 } 534 } 535 536 // EnableDax will enable dax service for this dynamodb session 537 func (d *DynamoDB) enableDaxInternal() error { 538 if d.cn == nil { 539 return errors.New("Enable Dax Failed: " + "DynamoDB Not Yet Connected") 540 } 541 542 cfg := dax.DefaultConfig() 543 cfg.HostPorts = []string{d.DaxEndpoint} 544 cfg.Region = d.AwsRegion.Key() 545 546 var err error 547 548 d.cnDax, err = dax.New(cfg) 549 550 if err != nil { 551 d.cnDax = nil 552 return errors.New("Enable Dax Failed: " + err.Error()) 553 } 554 555 // default skip dax to false 556 d.SkipDax = false 557 558 // success 559 return nil 560 } 561 562 // DisableDax will disable dax service for this dynamodb session 563 func (d *DynamoDB) DisableDax() { 564 if d.cnDax != nil { 565 _ = d.cnDax.Close() 566 d.cnDax = nil 567 d.SkipDax = false 568 } 569 } 570 571 // UpdateParentSegment updates this struct's xray parent segment, if no parent segment, set nil 572 func (d *DynamoDB) UpdateParentSegment(parentSegment *xray.XRayParentSegment) { 573 d._parentSegment = parentSegment 574 } 575 576 // do_PutItem is a helper that calls either dax or dynamodb based on dax availability 577 func (d *DynamoDB) do_PutItem(input *dynamodb.PutItemInput, ctx ...aws.Context) (output *dynamodb.PutItemOutput, err error) { 578 if d.cnDax != nil && !d.SkipDax { 579 // dax 580 if len(ctx) <= 0 { 581 return d.cnDax.PutItem(input) 582 } else { 583 return d.cnDax.PutItemWithContext(ctx[0], input) 584 } 585 } else if d.cn != nil { 586 // dynamodb 587 if len(ctx) <= 0 { 588 return d.cn.PutItem(input) 589 } else { 590 return d.cn.PutItemWithContext(ctx[0], input) 591 } 592 } else { 593 // connection error 594 return nil, errors.New("DynamoDB PutItem Failed: " + "No DynamoDB or Dax Connection Available") 595 } 596 } 597 598 // do_UpdateItem is a helper that calls either dax or dynamodb based on dax availability 599 func (d *DynamoDB) do_UpdateItem(input *dynamodb.UpdateItemInput, ctx ...aws.Context) (output *dynamodb.UpdateItemOutput, err error) { 600 if d.cnDax != nil && !d.SkipDax { 601 // dax 602 if len(ctx) <= 0 { 603 return d.cnDax.UpdateItem(input) 604 } else { 605 return d.cnDax.UpdateItemWithContext(ctx[0], input) 606 } 607 } else if d.cn != nil { 608 // dynamodb 609 if len(ctx) <= 0 { 610 return d.cn.UpdateItem(input) 611 } else { 612 return d.cn.UpdateItemWithContext(ctx[0], input) 613 } 614 } else { 615 // connection error 616 return nil, errors.New("DynamoDB UpdateItem Failed: " + "No DynamoDB or Dax Connection Available") 617 } 618 } 619 620 // do_DeleteItem is a helper that calls either dax or dynamodb based on dax availability 621 func (d *DynamoDB) do_DeleteItem(input *dynamodb.DeleteItemInput, ctx ...aws.Context) (output *dynamodb.DeleteItemOutput, err error) { 622 if d.cnDax != nil && !d.SkipDax { 623 // dax 624 if len(ctx) <= 0 { 625 return d.cnDax.DeleteItem(input) 626 } else { 627 return d.cnDax.DeleteItemWithContext(ctx[0], input) 628 } 629 } else if d.cn != nil { 630 // dynamodb 631 if len(ctx) <= 0 { 632 return d.cn.DeleteItem(input) 633 } else { 634 return d.cn.DeleteItemWithContext(ctx[0], input) 635 } 636 } else { 637 // connection error 638 return nil, errors.New("DynamoDB DeleteItem Failed: " + "No DynamoDB or Dax Connection Available") 639 } 640 } 641 642 // do_GetItem is a helper that calls either dax or dynamodb based on dax availability 643 func (d *DynamoDB) do_GetItem(input *dynamodb.GetItemInput, ctx ...aws.Context) (output *dynamodb.GetItemOutput, err error) { 644 if d.cnDax != nil && !d.SkipDax { 645 // dax 646 if len(ctx) <= 0 { 647 return d.cnDax.GetItem(input) 648 } else { 649 return d.cnDax.GetItemWithContext(ctx[0], input) 650 } 651 } else if d.cn != nil { 652 // dynamodb 653 if len(ctx) <= 0 { 654 return d.cn.GetItem(input) 655 } else { 656 return d.cn.GetItemWithContext(ctx[0], input) 657 } 658 } else { 659 // connection error 660 return nil, errors.New("DynamoDB GetItem Failed: " + "No DynamoDB or Dax Connection Available") 661 } 662 } 663 664 // do_Query_Pagination_Data is a helper that calls either dax or dynamodb based on dax availability, to get pagination data for the given query filter 665 func (d *DynamoDB) do_Query_Pagination_Data(input *dynamodb.QueryInput, ctx ...aws.Context) (paginationData []map[string]*dynamodb.AttributeValue, err error) { 666 paginationData = make([]map[string]*dynamodb.AttributeValue, 0) 667 668 if d.cnDax != nil && !d.SkipDax { 669 // dax 670 fn := func(pageOutput *dynamodb.QueryOutput, lastPage bool) bool { 671 if pageOutput != nil { 672 if pageOutput.Items != nil && len(pageOutput.Items) > 0 { 673 if pageOutput.LastEvaluatedKey != nil { 674 paginationData = append(paginationData, pageOutput.LastEvaluatedKey) 675 } 676 } 677 } 678 679 return !lastPage 680 } 681 682 if len(ctx) <= 0 { 683 err = d.cnDax.QueryPages(input, fn) 684 } else { 685 err = d.cnDax.QueryPagesWithContext(ctx[0], input, fn) 686 } 687 688 return paginationData, err 689 690 } else if d.cn != nil { 691 // dynamodb 692 fn := func(pageOutput *dynamodb.QueryOutput, lastPage bool) bool { 693 if pageOutput != nil { 694 if pageOutput.Items != nil && len(pageOutput.Items) > 0 { 695 if pageOutput.LastEvaluatedKey != nil { 696 paginationData = append(paginationData, pageOutput.LastEvaluatedKey) 697 } 698 } 699 } 700 701 return !lastPage 702 } 703 704 if len(ctx) <= 0 { 705 err = d.cn.QueryPages(input, fn) 706 } else { 707 err = d.cn.QueryPagesWithContext(ctx[0], input, fn) 708 } 709 710 return paginationData, err 711 712 } else { 713 // connection error 714 return nil, errors.New("DynamoDB QueryPaginationData Failed: " + "No DynamoDB or Dax Connection Available") 715 } 716 } 717 718 // do_Query is a helper that calls either dax or dynamodb based on dax availability 719 func (d *DynamoDB) do_Query(input *dynamodb.QueryInput, pagedQuery bool, pagedQueryPageCountLimit *int64, ctx ...aws.Context) (output *dynamodb.QueryOutput, err error) { 720 if d.cnDax != nil && !d.SkipDax { 721 // dax 722 if !pagedQuery { 723 // 724 // not paged query 725 // 726 if len(ctx) <= 0 { 727 return d.cnDax.Query(input) 728 } else { 729 return d.cnDax.QueryWithContext(ctx[0], input) 730 } 731 } else { 732 // 733 // paged query 734 // 735 pageCount := int64(0) 736 737 fn := func(pageOutput *dynamodb.QueryOutput, lastPage bool) bool { 738 if pageOutput != nil { 739 if pageOutput.Items != nil && len(pageOutput.Items) > 0 { 740 pageCount++ 741 742 if output == nil { 743 output = new(dynamodb.QueryOutput) 744 } 745 746 output.SetCount(aws.Int64Value(output.Count) + aws.Int64Value(pageOutput.Count)) 747 output.SetScannedCount(aws.Int64Value(output.ScannedCount) + aws.Int64Value(pageOutput.ScannedCount)) 748 output.SetLastEvaluatedKey(pageOutput.LastEvaluatedKey) 749 750 for _, v := range pageOutput.Items { 751 output.Items = append(output.Items, v) 752 } 753 754 // check if ok to stop 755 if pagedQueryPageCountLimit != nil && *pagedQueryPageCountLimit > 0 { 756 if pageCount >= *pagedQueryPageCountLimit { 757 return false 758 } 759 } 760 } 761 } 762 763 return !lastPage 764 } 765 766 if len(ctx) <= 0 { 767 err = d.cnDax.QueryPages(input, fn) 768 } else { 769 err = d.cnDax.QueryPagesWithContext(ctx[0], input, fn) 770 } 771 772 return output, err 773 } 774 } else if d.cn != nil { 775 // dynamodb 776 if !pagedQuery { 777 // 778 // not paged query 779 // 780 if len(ctx) <= 0 { 781 return d.cn.Query(input) 782 } else { 783 return d.cn.QueryWithContext(ctx[0], input) 784 } 785 } else { 786 // 787 // paged query 788 // 789 pageCount := int64(0) 790 791 fn := func(pageOutput *dynamodb.QueryOutput, lastPage bool) bool { 792 if pageOutput != nil { 793 if pageOutput.Items != nil && len(pageOutput.Items) > 0 { 794 pageCount++ 795 796 if output == nil { 797 output = new(dynamodb.QueryOutput) 798 } 799 800 output.SetCount(aws.Int64Value(output.Count) + aws.Int64Value(pageOutput.Count)) 801 output.SetScannedCount(aws.Int64Value(output.ScannedCount) + aws.Int64Value(pageOutput.ScannedCount)) 802 output.SetLastEvaluatedKey(pageOutput.LastEvaluatedKey) 803 804 for _, v := range pageOutput.Items { 805 output.Items = append(output.Items, v) 806 } 807 808 // check if ok to stop 809 if pagedQueryPageCountLimit != nil && *pagedQueryPageCountLimit > 0 { 810 if pageCount >= *pagedQueryPageCountLimit { 811 return false 812 } 813 } 814 } 815 } 816 817 return !lastPage 818 } 819 820 if len(ctx) <= 0 { 821 err = d.cn.QueryPages(input, fn) 822 } else { 823 err = d.cn.QueryPagesWithContext(ctx[0], input, fn) 824 } 825 826 return output, err 827 } 828 } else { 829 // connection error 830 return nil, errors.New("DynamoDB QueryItems Failed: " + "No DynamoDB or Dax Connection Available") 831 } 832 } 833 834 // do_Scan is a helper that calls either dax or dynamodb based on dax availability 835 func (d *DynamoDB) do_Scan(input *dynamodb.ScanInput, pagedQuery bool, pagedQueryPageCountLimit *int64, ctx ...aws.Context) (output *dynamodb.ScanOutput, err error) { 836 if d.cnDax != nil && !d.SkipDax { 837 // dax 838 if !pagedQuery { 839 // 840 // not paged query 841 // 842 if len(ctx) <= 0 { 843 return d.cnDax.Scan(input) 844 } else { 845 return d.cnDax.ScanWithContext(ctx[0], input) 846 } 847 } else { 848 // 849 // paged query 850 // 851 pageCount := int64(0) 852 853 fn := func(pageOutput *dynamodb.ScanOutput, lastPage bool) bool { 854 if pageOutput != nil { 855 if pageOutput.Items != nil && len(pageOutput.Items) > 0 { 856 pageCount++ 857 858 if output == nil { 859 output = new(dynamodb.ScanOutput) 860 } 861 862 output.SetCount(aws.Int64Value(output.Count) + aws.Int64Value(pageOutput.Count)) 863 output.SetScannedCount(aws.Int64Value(output.ScannedCount) + aws.Int64Value(pageOutput.ScannedCount)) 864 output.SetLastEvaluatedKey(pageOutput.LastEvaluatedKey) 865 866 for _, v := range pageOutput.Items { 867 output.Items = append(output.Items, v) 868 } 869 870 if pagedQueryPageCountLimit != nil && *pagedQueryPageCountLimit > 0 { 871 if pageCount >= *pagedQueryPageCountLimit { 872 return false 873 } 874 } 875 } 876 } 877 878 return !lastPage 879 } 880 881 if len(ctx) <= 0 { 882 err = d.cnDax.ScanPages(input, fn) 883 } else { 884 err = d.cnDax.ScanPagesWithContext(ctx[0], input, fn) 885 } 886 887 return output, err 888 } 889 } else if d.cn != nil { 890 // dynamodb 891 if !pagedQuery { 892 // 893 // not paged query 894 // 895 if len(ctx) <= 0 { 896 return d.cn.Scan(input) 897 } else { 898 return d.cn.ScanWithContext(ctx[0], input) 899 } 900 } else { 901 // 902 // paged query 903 // 904 pageCount := int64(0) 905 906 fn := func(pageOutput *dynamodb.ScanOutput, lastPage bool) bool { 907 if pageOutput != nil { 908 if pageOutput.Items != nil && len(pageOutput.Items) > 0 { 909 pageCount++ 910 911 if output == nil { 912 output = new(dynamodb.ScanOutput) 913 } 914 915 output.SetCount(aws.Int64Value(output.Count) + aws.Int64Value(pageOutput.Count)) 916 output.SetScannedCount(aws.Int64Value(output.ScannedCount) + aws.Int64Value(pageOutput.ScannedCount)) 917 output.SetLastEvaluatedKey(pageOutput.LastEvaluatedKey) 918 919 for _, v := range pageOutput.Items { 920 output.Items = append(output.Items, v) 921 } 922 923 if pagedQueryPageCountLimit != nil && *pagedQueryPageCountLimit > 0 { 924 if pageCount >= *pagedQueryPageCountLimit { 925 return false 926 } 927 } 928 } 929 } 930 931 return !lastPage 932 } 933 934 if len(ctx) <= 0 { 935 err = d.cn.ScanPages(input, fn) 936 } else { 937 err = d.cn.ScanPagesWithContext(ctx[0], input, fn) 938 } 939 940 return output, err 941 } 942 } else { 943 // connection error 944 return nil, errors.New("DynamoDB ScanItems Failed: " + "No DynamoDB or Dax Connection Available") 945 } 946 } 947 948 // do_BatchWriteItem is a helper that calls either dax or dynamodb based on dax availability 949 func (d *DynamoDB) do_BatchWriteItem(input *dynamodb.BatchWriteItemInput, ctx ...aws.Context) (output *dynamodb.BatchWriteItemOutput, err error) { 950 if d.cnDax != nil && !d.SkipDax { 951 // dax 952 if len(ctx) <= 0 { 953 return d.cnDax.BatchWriteItem(input) 954 } else { 955 return d.cnDax.BatchWriteItemWithContext(ctx[0], input) 956 } 957 } else if d.cn != nil { 958 // dynamodb 959 if len(ctx) <= 0 { 960 return d.cn.BatchWriteItem(input) 961 } else { 962 return d.cn.BatchWriteItemWithContext(ctx[0], input) 963 } 964 } else { 965 // connection error 966 return nil, errors.New("DynamoDB BatchWriteItem Failed: " + "No DynamoDB or Dax Connection Available") 967 } 968 } 969 970 // do_BatchGetItem is a helper that calls either dax or dynamodb based on dax availability 971 func (d *DynamoDB) do_BatchGetItem(input *dynamodb.BatchGetItemInput, ctx ...aws.Context) (output *dynamodb.BatchGetItemOutput, err error) { 972 if d.cnDax != nil && !d.SkipDax { 973 // dax 974 if len(ctx) <= 0 { 975 return d.cnDax.BatchGetItem(input) 976 } else { 977 return d.cnDax.BatchGetItemWithContext(ctx[0], input) 978 } 979 } else if d.cn != nil { 980 // dynamodb 981 if len(ctx) <= 0 { 982 return d.cn.BatchGetItem(input) 983 } else { 984 return d.cn.BatchGetItemWithContext(ctx[0], input) 985 } 986 } else { 987 // connection error 988 return nil, errors.New("DynamoDB BatchGetItem Failed: " + "No DynamoDB or Dax Connection Available") 989 } 990 } 991 992 // do_TransactWriteItems is a helper that calls either dax or dynamodb based on dax availability 993 func (d *DynamoDB) do_TransactWriteItems(input *dynamodb.TransactWriteItemsInput, ctx ...aws.Context) (output *dynamodb.TransactWriteItemsOutput, err error) { 994 if d.cnDax != nil && !d.SkipDax { 995 // dax 996 if len(ctx) <= 0 { 997 return d.cnDax.TransactWriteItems(input) 998 } else { 999 return d.cnDax.TransactWriteItemsWithContext(ctx[0], input) 1000 } 1001 } else if d.cn != nil { 1002 // dynamodb 1003 if len(ctx) <= 0 { 1004 return d.cn.TransactWriteItems(input) 1005 } else { 1006 return d.cn.TransactWriteItemsWithContext(ctx[0], input) 1007 } 1008 } else { 1009 // connection error 1010 return nil, errors.New("DynamoDB TransactionWriteItems Failed: " + "No DynamoDB or Dax Connection Available") 1011 } 1012 } 1013 1014 // do_TransactGetItems is a helper that calls either dax or dynamodb based on dax availability 1015 func (d *DynamoDB) do_TransactGetItems(input *dynamodb.TransactGetItemsInput, ctx ...aws.Context) (output *dynamodb.TransactGetItemsOutput, err error) { 1016 if d.cnDax != nil && !d.SkipDax { 1017 // dax 1018 if len(ctx) <= 0 { 1019 return d.cnDax.TransactGetItems(input) 1020 } else { 1021 return d.cnDax.TransactGetItemsWithContext(ctx[0], input) 1022 } 1023 } else if d.cn != nil { 1024 // dynamodb 1025 if len(ctx) <= 0 { 1026 return d.cn.TransactGetItems(input) 1027 } else { 1028 return d.cn.TransactGetItemsWithContext(ctx[0], input) 1029 } 1030 } else { 1031 // connection error 1032 return nil, errors.New("DynamoDB TransactionGetItems Failed: " + "No DynamoDB or Dax Connection Available") 1033 } 1034 } 1035 1036 func (d *DynamoDB) TimeOutDuration(timeOutSeconds uint) *time.Duration { 1037 if timeOutSeconds == 0 { 1038 return nil 1039 } else { 1040 return util.DurationPtr(time.Duration(timeOutSeconds) * time.Second) 1041 } 1042 } 1043 1044 // PutItem will add or update a new item into dynamodb table 1045 // 1046 // parameters: 1047 // 1048 // item = required, must be a struct object; ALWAYS SINGLE STRUCT OBJECT, NEVER SLICE 1049 // must start with fields 'pk string', 'sk string', and 'data string' before any other attributes 1050 // timeOutDuration = optional, timeout duration sent via context to scan method; nil if not using timeout duration 1051 // 1052 // notes: 1053 // 1054 // item struct tags 1055 // use `json:"" dynamodbav:""` 1056 // json = sets the name used in json 1057 // dynamodbav = sets the name used in dynamodb 1058 // reference child element 1059 // if struct has field with complex type (another struct), to reference it in code, use the parent struct field dot child field notation 1060 // Info in parent struct with struct tag as info; to reach child element: info.xyz 1061 func (d *DynamoDB) PutItem(item interface{}, timeOutDuration *time.Duration) (ddbErr *DynamoDBError) { 1062 if xray.XRayServiceOn() { 1063 return d.putItemWithTrace(item, timeOutDuration) 1064 } else { 1065 return d.putItemNormal(item, timeOutDuration) 1066 } 1067 } 1068 1069 func (d *DynamoDB) putItemWithTrace(item interface{}, timeOutDuration *time.Duration) (ddbErr *DynamoDBError) { 1070 trace := xray.NewSegment("DynamoDB-PutItem", d._parentSegment) 1071 defer trace.Close() 1072 defer func() { 1073 if ddbErr != nil { 1074 _ = trace.Seg.AddError(fmt.Errorf(ddbErr.ErrorMessage)) 1075 } 1076 }() 1077 1078 if d.cn == nil { 1079 ddbErr = d.handleError(errors.New("DynamoDB Connection is Required")) 1080 return ddbErr 1081 } 1082 1083 if util.LenTrim(d.TableName) <= 0 { 1084 ddbErr = d.handleError(errors.New("DynamoDB Table Name is Required")) 1085 return ddbErr 1086 } 1087 1088 if item == nil { 1089 ddbErr = d.handleError(errors.New("DynamoDB PutItem Failed: " + "Input Item Object is Nil")) 1090 return ddbErr 1091 } 1092 1093 trace.Capture("PutItem", func() error { 1094 if av, err := dynamodbattribute.MarshalMap(item); err != nil { 1095 ddbErr = d.handleError(err, "DynamoDB PutItem Failed: (MarshalMap)") 1096 return fmt.Errorf(ddbErr.ErrorMessage) 1097 } else { 1098 input := &dynamodb.PutItemInput{ 1099 Item: av, 1100 TableName: aws.String(d.TableName), 1101 } 1102 1103 // record params payload 1104 d.LastExecuteParamsPayload = "PutItem = " + input.String() 1105 1106 subTrace := trace.NewSubSegment("PutItem_Do") 1107 defer subTrace.Close() 1108 1109 // save into dynamodb table 1110 if timeOutDuration != nil { 1111 ctx, cancel := context.WithTimeout(subTrace.Ctx, *timeOutDuration) 1112 defer cancel() 1113 _, err = d.do_PutItem(input, ctx) 1114 } else { 1115 _, err = d.do_PutItem(input, subTrace.Ctx) 1116 } 1117 1118 if err != nil { 1119 ddbErr = d.handleError(err, "DynamoDB PutItem Failed: (PutItem)") 1120 return fmt.Errorf(ddbErr.ErrorMessage) 1121 } else { 1122 return nil 1123 } 1124 } 1125 }, &xray.XTraceData{ 1126 Meta: map[string]interface{}{ 1127 "TableName": d.TableName, 1128 "ItemInfo": item, 1129 }, 1130 }) 1131 1132 // put item was successful 1133 return ddbErr 1134 } 1135 1136 func (d *DynamoDB) putItemNormal(item interface{}, timeOutDuration *time.Duration) (ddbErr *DynamoDBError) { 1137 if d.cn == nil { 1138 return d.handleError(errors.New("DynamoDB Connection is Required")) 1139 } 1140 1141 if util.LenTrim(d.TableName) <= 0 { 1142 return d.handleError(errors.New("DynamoDB Table Name is Required")) 1143 } 1144 1145 if item == nil { 1146 return d.handleError(errors.New("DynamoDB PutItem Failed: " + "Input Item Object is Nil")) 1147 } 1148 1149 if av, err := dynamodbattribute.MarshalMap(item); err != nil { 1150 ddbErr = d.handleError(err, "DynamoDB PutItem Failed: (MarshalMap)") 1151 } else { 1152 input := &dynamodb.PutItemInput{ 1153 Item: av, 1154 TableName: aws.String(d.TableName), 1155 } 1156 1157 // record params payload 1158 d.LastExecuteParamsPayload = "PutItem = " + input.String() 1159 1160 // save into dynamodb table 1161 if timeOutDuration != nil { 1162 ctx, cancel := context.WithTimeout(context.Background(), *timeOutDuration) 1163 defer cancel() 1164 _, err = d.do_PutItem(input, ctx) 1165 } else { 1166 _, err = d.do_PutItem(input) 1167 } 1168 1169 if err != nil { 1170 ddbErr = d.handleError(err, "DynamoDB PutItem Failed: (PutItem)") 1171 } else { 1172 ddbErr = nil 1173 } 1174 } 1175 1176 // put item was successful 1177 return ddbErr 1178 } 1179 1180 // PutItemWithRetry add or updates, and handles dynamodb retries in case action temporarily fails 1181 func (d *DynamoDB) PutItemWithRetry(maxRetries uint, item interface{}, timeOutDuration *time.Duration) *DynamoDBError { 1182 if maxRetries > 10 { 1183 maxRetries = 10 1184 } 1185 1186 timeout := 5 * time.Second 1187 1188 if timeOutDuration != nil { 1189 timeout = *timeOutDuration 1190 } 1191 1192 if timeout < 5*time.Second { 1193 timeout = 5 * time.Second 1194 } else if timeout > 15*time.Second { 1195 timeout = 15 * time.Second 1196 } 1197 1198 if err := d.PutItem(item, util.DurationPtr(timeout)); err != nil { 1199 // has error 1200 if maxRetries > 0 { 1201 if err.AllowRetry { 1202 if err.RetryNeedsBackOff { 1203 time.Sleep(500 * time.Millisecond) 1204 } else { 1205 time.Sleep(100 * time.Millisecond) 1206 } 1207 1208 log.Println("PutItemWithRetry Failed: " + err.ErrorMessage) 1209 return d.PutItemWithRetry(maxRetries-1, item, util.DurationPtr(timeout)) 1210 } else { 1211 if err.SuppressError { 1212 log.Println("PutItemWithRetry DynamoDB Error Suppressed, Returning Error Nil (MaxRetries = " + util.UintToStr(maxRetries) + ")") 1213 return nil 1214 } else { 1215 return &DynamoDBError{ 1216 ErrorMessage: "PutItemWithRetry Failed: " + err.ErrorMessage, 1217 SuppressError: false, 1218 AllowRetry: false, 1219 RetryNeedsBackOff: false, 1220 } 1221 } 1222 } 1223 } else { 1224 if err.SuppressError { 1225 log.Println("PutItemWithRetry DynamoDB Error Suppressed, Returning Error Nil (MaxRetries = 0)") 1226 return nil 1227 } else { 1228 return &DynamoDBError{ 1229 ErrorMessage: "PutItemWithRetry Failed: (MaxRetries = 0) " + err.ErrorMessage, 1230 SuppressError: false, 1231 AllowRetry: false, 1232 RetryNeedsBackOff: false, 1233 } 1234 } 1235 } 1236 } else { 1237 // no error 1238 return nil 1239 } 1240 } 1241 1242 // UpdateItem will update dynamodb item in given table using primary key (PK, SK), and set specific attributes with new value and persists 1243 // UpdateItem requires using Primary Key attributes, and limited to TWO key attributes in condition maximum; 1244 // 1245 // important 1246 // 1247 // if dynamodb table is defined as PK and SK together, then to search, MUST use PK and SK together or error will trigger 1248 // 1249 // parameters: 1250 // 1251 // pkValue = required, value of partition key to seek 1252 // skValue = optional, value of sort key to seek; set to blank if value not provided 1253 // 1254 // updateExpression = required, ATTRIBUTES ARE CASE SENSITIVE; set remove add or delete action expression, see Rules URL for full detail 1255 // Rules: 1256 // 1) https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.UpdateExpressions.html 1257 // Usage Syntax: 1258 // 1) Action Keywords are: set, add, remove, delete 1259 // 2) Each Action Keyword May Appear in UpdateExpression Only Once 1260 // 3) Each Action Keyword Grouping May Contain One or More Actions, Such as 'set price=:p, age=:age, etc' (each action separated by comma) 1261 // 4) Each Action Keyword Always Begin with Action Keyword itself, such as 'set ...', 'add ...', etc 1262 // 5) If Attribute is Numeric, Action Can Perform + or - Operation in Expression, such as 'set age=age-:newAge, price=price+:price, etc' 1263 // 6) If Attribute is Slice, Action Can Perform Slice Element Operation in Expression, such as 'set age[2]=:newData, etc' 1264 // 7) When Attribute Name is Reserved Keyword, Use ExpressionAttributeNames to Define #xyz to Alias 1265 // a) Use the #xyz in the KeyConditionExpression such as #yr = :year (:year is Defined ExpressionAttributeValue) 1266 // 8) When Attribute is a List, Use list_append(a, b, ...) in Expression to append elements (list_append() is case sensitive) 1267 // a) set #ri = list_append(#ri, :vals) where :vals represents one or more of elements to add as in L 1268 // 9) if_not_exists(path, value) 1269 // a) Avoids existing attribute if already exists 1270 // b) set price = if_not_exists(price, :p) 1271 // c) if_not_exists is case sensitive; path is the existing attribute to check 1272 // 10) Action Type Purposes 1273 // a) SET = add one or more attributes to an item; overrides existing attributes in item with new values; if attribute is number, able to perform + or - operations 1274 // b) REMOVE = remove one or more attributes from an item, to remove multiple attributes, separate by comma; remove element from list use xyz[1] index notation 1275 // c) ADD = adds a new attribute and its values to an item; if attribute is number and already exists, value will add up or subtract 1276 // d) DELETE = supports only on set data types; deletes one or more elements from a set, such as 'delete color :c' 1277 // 11) Example 1278 // a) set age=:age, name=:name, etc 1279 // b) set age=age-:age, num=num+:num, etc 1280 // 1281 // conditionExpress = optional, ATTRIBUTES ARE CASE SENSITIVE; sets conditions for this condition expression, set to blank if not used 1282 // Usage Syntax: 1283 // 1) "size(info.actors) >= :num" 1284 // a) When Length of Actors Attribute Value is Equal or Greater Than :num, ONLY THEN UpdateExpression is Performed 1285 // 2) ExpressionAttributeName and ExpressionAttributeValue is Still Defined within ExpressionAttributeNames and ExpressionAttributeValues Where Applicable 1286 // 1287 // expressionAttributeNames = optional, ATTRIBUTES ARE CASE SENSITIVE; set nil if not used, must define for attribute names that are reserved keywords such as year, data etc. using #xyz 1288 // Usage Syntax: 1289 // 1) map[string]*string: where string is the #xyz, and *string is the original xyz attribute name 1290 // a) map[string]*string { "#xyz": aws.String("Xyz"), } 1291 // 2) Add to Map 1292 // a) m := make(map[string]*string) 1293 // b) m["#xyz"] = aws.String("Xyz") 1294 // 1295 // expressionAttributeValues = required, ATTRIBUTES ARE CASE SENSITIVE; sets the value token and value actual to be used within the keyConditionExpression; this sets both compare token and compare value 1296 // Usage Syntax: 1297 // 1) map[string]*dynamodb.AttributeValue: where string is the :xyz, and *dynamodb.AttributeValue is { S: aws.String("abc"), }, 1298 // a) map[string]*dynamodb.AttributeValue { ":xyz" : { S: aws.String("abc"), }, ":xyy" : { N: aws.String("123"), }, } 1299 // 2) Add to Map 1300 // a) m := make(map[string]*dynamodb.AttributeValue) 1301 // b) m[":xyz"] = &dynamodb.AttributeValue{ S: aws.String("xyz") } 1302 // 3) Slice of Strings -> CONVERT To Slice of *dynamodb.AttributeValue = []string -> []*dynamodb.AttributeValue 1303 // a) av, err := dynamodbattribute.MarshalList(xyzSlice) 1304 // b) ExpressionAttributeValue, Use 'L' To Represent the List for av defined in 3.a above 1305 // 1306 // timeOutDuration = optional, timeout duration sent via context to scan method; nil if not using timeout duration 1307 // 1308 // notes: 1309 // 1310 // item struct tags 1311 // use `json:"" dynamodbav:""` 1312 // json = sets the name used in json 1313 // dynamodbav = sets the name used in dynamodb 1314 // reference child element 1315 // if struct has field with complex type (another struct), to reference it in code, use the parent struct field dot child field notation 1316 // Info in parent struct with struct tag as info; to reach child element: info.xyz 1317 func (d *DynamoDB) UpdateItem(pkValue string, skValue string, 1318 updateExpression string, 1319 conditionExpression string, 1320 expressionAttributeNames map[string]*string, 1321 expressionAttributeValues map[string]*dynamodb.AttributeValue, 1322 timeOutDuration *time.Duration) (ddbErr *DynamoDBError) { 1323 1324 if xray.XRayServiceOn() { 1325 return d.updateItemWithTrace(pkValue, skValue, updateExpression, conditionExpression, expressionAttributeNames, expressionAttributeValues, timeOutDuration) 1326 } else { 1327 return d.updateItemNormal(pkValue, skValue, updateExpression, conditionExpression, expressionAttributeNames, expressionAttributeValues, timeOutDuration) 1328 } 1329 } 1330 1331 func (d *DynamoDB) updateItemWithTrace(pkValue string, skValue string, 1332 updateExpression string, 1333 conditionExpression string, 1334 expressionAttributeNames map[string]*string, 1335 expressionAttributeValues map[string]*dynamodb.AttributeValue, 1336 timeOutDuration *time.Duration) (ddbErr *DynamoDBError) { 1337 1338 trace := xray.NewSegment("DynamoDB-UpdateItem", d._parentSegment) 1339 defer trace.Close() 1340 defer func() { 1341 if ddbErr != nil { 1342 _ = trace.Seg.AddError(fmt.Errorf(ddbErr.ErrorMessage)) 1343 } 1344 }() 1345 1346 if d.cn == nil { 1347 ddbErr = d.handleError(errors.New("DynamoDB Connection is Required")) 1348 return ddbErr 1349 } 1350 1351 if util.LenTrim(d.TableName) <= 0 { 1352 ddbErr = d.handleError(errors.New("DynamoDB Table Name is Required")) 1353 return ddbErr 1354 } 1355 1356 // validate input parameters 1357 if util.LenTrim(d.PKName) <= 0 { 1358 ddbErr = d.handleError(errors.New("DynamoDB UpdateItem Failed: " + "PK Name is Required")) 1359 return ddbErr 1360 } 1361 1362 if util.LenTrim(pkValue) <= 0 { 1363 ddbErr = d.handleError(errors.New("DynamoDB UpdateItem Failed: " + "PK Value is Required")) 1364 return ddbErr 1365 } 1366 1367 if util.LenTrim(skValue) > 0 { 1368 if util.LenTrim(d.SKName) <= 0 { 1369 ddbErr = d.handleError(errors.New("DynamoDB UpdateItem Failed: " + "SK Name is Required")) 1370 return ddbErr 1371 } 1372 } 1373 1374 if util.LenTrim(updateExpression) <= 0 { 1375 ddbErr = d.handleError(errors.New("DynamoDB UpdateItem Failed: " + "UpdateExpression is Required")) 1376 return ddbErr 1377 } 1378 1379 if expressionAttributeValues == nil { 1380 ddbErr = d.handleError(errors.New("DynamoDB UpdateItem Failed: " + "ExpressionAttributeValues is Required")) 1381 return ddbErr 1382 } 1383 1384 trace.Capture("UpdateItem", func() error { 1385 // define key 1386 m := make(map[string]*dynamodb.AttributeValue) 1387 1388 m[d.PKName] = &dynamodb.AttributeValue{S: aws.String(pkValue)} 1389 1390 if util.LenTrim(skValue) > 0 { 1391 m[d.SKName] = &dynamodb.AttributeValue{S: aws.String(skValue)} 1392 } 1393 1394 // build update item input params 1395 params := &dynamodb.UpdateItemInput{ 1396 TableName: aws.String(d.TableName), 1397 Key: m, 1398 UpdateExpression: aws.String(updateExpression), 1399 ExpressionAttributeValues: expressionAttributeValues, 1400 ReturnValues: aws.String(dynamodb.ReturnValueAllNew), 1401 } 1402 1403 if util.LenTrim(conditionExpression) > 0 { 1404 params.ConditionExpression = aws.String(conditionExpression) 1405 } 1406 1407 if expressionAttributeNames != nil { 1408 params.ExpressionAttributeNames = expressionAttributeNames 1409 } 1410 1411 // record params payload 1412 d.LastExecuteParamsPayload = "UpdateItem = " + params.String() 1413 1414 // execute dynamodb service 1415 var err error 1416 1417 subTrace := trace.NewSubSegment("UpdateItem_Do") 1418 defer subTrace.Close() 1419 1420 // create timeout context 1421 if timeOutDuration != nil { 1422 ctx, cancel := context.WithTimeout(subTrace.Ctx, *timeOutDuration) 1423 defer cancel() 1424 _, err = d.do_UpdateItem(params, ctx) 1425 } else { 1426 _, err = d.do_UpdateItem(params, subTrace.Ctx) 1427 } 1428 1429 if err != nil { 1430 ddbErr = d.handleError(err, "DynamoDB UpdateItem Failed: (UpdateItem)") 1431 return fmt.Errorf(ddbErr.ErrorMessage) 1432 } else { 1433 return nil 1434 } 1435 }, &xray.XTraceData{ 1436 Meta: map[string]interface{}{ 1437 "TableName": d.TableName, 1438 "PK": pkValue, 1439 "SK": skValue, 1440 "UpdateExpression": updateExpression, 1441 "ConditionExpress": conditionExpression, 1442 "ExpressionAttributeNames": expressionAttributeNames, 1443 "ExpressionAttributeValues": expressionAttributeValues, 1444 }, 1445 }) 1446 1447 // update item successful 1448 return ddbErr 1449 } 1450 1451 func (d *DynamoDB) updateItemNormal(pkValue string, skValue string, 1452 updateExpression string, 1453 conditionExpression string, 1454 expressionAttributeNames map[string]*string, 1455 expressionAttributeValues map[string]*dynamodb.AttributeValue, 1456 timeOutDuration *time.Duration) (ddbErr *DynamoDBError) { 1457 1458 if d.cn == nil { 1459 return d.handleError(errors.New("DynamoDB Connection is Required")) 1460 } 1461 1462 if util.LenTrim(d.TableName) <= 0 { 1463 return d.handleError(errors.New("DynamoDB Table Name is Required")) 1464 } 1465 1466 // validate input parameters 1467 if util.LenTrim(d.PKName) <= 0 { 1468 return d.handleError(errors.New("DynamoDB UpdateItem Failed: " + "PK Name is Required")) 1469 } 1470 1471 if util.LenTrim(pkValue) <= 0 { 1472 return d.handleError(errors.New("DynamoDB UpdateItem Failed: " + "PK Value is Required")) 1473 } 1474 1475 if util.LenTrim(skValue) > 0 { 1476 if util.LenTrim(d.SKName) <= 0 { 1477 return d.handleError(errors.New("DynamoDB UpdateItem Failed: " + "SK Name is Required")) 1478 } 1479 } 1480 1481 if util.LenTrim(updateExpression) <= 0 { 1482 return d.handleError(errors.New("DynamoDB UpdateItem Failed: " + "UpdateExpression is Required")) 1483 } 1484 1485 if expressionAttributeValues == nil { 1486 return d.handleError(errors.New("DynamoDB UpdateItem Failed: " + "ExpressionAttributeValues is Required")) 1487 } 1488 1489 // define key 1490 m := make(map[string]*dynamodb.AttributeValue) 1491 1492 m[d.PKName] = &dynamodb.AttributeValue{S: aws.String(pkValue)} 1493 1494 if util.LenTrim(skValue) > 0 { 1495 m[d.SKName] = &dynamodb.AttributeValue{S: aws.String(skValue)} 1496 } 1497 1498 // build update item input params 1499 params := &dynamodb.UpdateItemInput{ 1500 TableName: aws.String(d.TableName), 1501 Key: m, 1502 UpdateExpression: aws.String(updateExpression), 1503 ExpressionAttributeValues: expressionAttributeValues, 1504 ReturnValues: aws.String(dynamodb.ReturnValueAllNew), 1505 } 1506 1507 if util.LenTrim(conditionExpression) > 0 { 1508 params.ConditionExpression = aws.String(conditionExpression) 1509 } 1510 1511 if expressionAttributeNames != nil { 1512 params.ExpressionAttributeNames = expressionAttributeNames 1513 } 1514 1515 // record params payload 1516 d.LastExecuteParamsPayload = "UpdateItem = " + params.String() 1517 1518 // execute dynamodb service 1519 var err error 1520 1521 // create timeout context 1522 if timeOutDuration != nil { 1523 ctx, cancel := context.WithTimeout(context.Background(), *timeOutDuration) 1524 defer cancel() 1525 _, err = d.do_UpdateItem(params, ctx) 1526 } else { 1527 _, err = d.do_UpdateItem(params) 1528 } 1529 1530 if err != nil { 1531 ddbErr = d.handleError(err, "DynamoDB UpdateItem Failed: (UpdateItem)") 1532 } else { 1533 ddbErr = nil 1534 } 1535 1536 // update item successful 1537 return ddbErr 1538 } 1539 1540 // RemoveItemAttribute will remove attribute from dynamodb item in given table using primary key (PK, SK) 1541 func (d *DynamoDB) RemoveItemAttribute(pkValue string, skValue string, removeExpression string, timeOutDuration *time.Duration) (ddbErr *DynamoDBError) { 1542 if xray.XRayServiceOn() { 1543 return d.removeItemAttributeWithTrace(pkValue, skValue, removeExpression, timeOutDuration) 1544 } else { 1545 return d.removeItemAttributeNormal(pkValue, skValue, removeExpression, timeOutDuration) 1546 } 1547 } 1548 1549 func (d *DynamoDB) removeItemAttributeWithTrace(pkValue string, skValue string, removeExpression string, timeOutDuration *time.Duration) (ddbErr *DynamoDBError) { 1550 trace := xray.NewSegment("DynamoDB-RemoveItemAttribute", d._parentSegment) 1551 defer trace.Close() 1552 defer func() { 1553 if ddbErr != nil { 1554 _ = trace.Seg.AddError(fmt.Errorf(ddbErr.ErrorMessage)) 1555 } 1556 }() 1557 1558 if d.cn == nil { 1559 ddbErr = d.handleError(errors.New("DynamoDB Connection is Required")) 1560 return ddbErr 1561 } 1562 1563 if util.LenTrim(d.TableName) <= 0 { 1564 ddbErr = d.handleError(errors.New("DynamoDB Table Name is Required")) 1565 return ddbErr 1566 } 1567 1568 // validate input parameters 1569 if util.LenTrim(d.PKName) <= 0 { 1570 ddbErr = d.handleError(errors.New("DynamoDB RemoveItemAttribute Failed: " + "PK Name is Required")) 1571 return ddbErr 1572 } 1573 1574 if util.LenTrim(pkValue) <= 0 { 1575 ddbErr = d.handleError(errors.New("DynamoDB RemoveItemAttribute Failed: " + "PK Value is Required")) 1576 return ddbErr 1577 } 1578 1579 if util.LenTrim(skValue) > 0 { 1580 if util.LenTrim(d.SKName) <= 0 { 1581 ddbErr = d.handleError(errors.New("DynamoDB RemoveItemAttribute Failed: " + "SK Name is Required")) 1582 return ddbErr 1583 } 1584 } 1585 1586 if util.LenTrim(removeExpression) <= 0 { 1587 ddbErr = d.handleError(errors.New("DynamoDB RemoveItemAttribute Failed: " + "RemoveExpression is Required")) 1588 return ddbErr 1589 } 1590 1591 trace.Capture("RemoveItemAttribute", func() error { 1592 // define key 1593 m := make(map[string]*dynamodb.AttributeValue) 1594 1595 m[d.PKName] = &dynamodb.AttributeValue{S: aws.String(pkValue)} 1596 1597 if util.LenTrim(skValue) > 0 { 1598 m[d.SKName] = &dynamodb.AttributeValue{S: aws.String(skValue)} 1599 } 1600 1601 // build update item input params for remove item attribute action 1602 params := &dynamodb.UpdateItemInput{ 1603 TableName: aws.String(d.TableName), 1604 Key: m, 1605 UpdateExpression: aws.String(removeExpression), 1606 ReturnValues: aws.String(dynamodb.ReturnValueAllNew), 1607 } 1608 1609 // record params payload 1610 d.LastExecuteParamsPayload = "RemoveItemAttribute = " + params.String() 1611 1612 // execute dynamodb service 1613 var err error 1614 1615 subTrace := trace.NewSubSegment("RemoveItemAttribute_Do") 1616 defer subTrace.Close() 1617 1618 // create timeout context 1619 if timeOutDuration != nil { 1620 ctx, cancel := context.WithTimeout(subTrace.Ctx, *timeOutDuration) 1621 defer cancel() 1622 _, err = d.do_UpdateItem(params, ctx) 1623 } else { 1624 _, err = d.do_UpdateItem(params, subTrace.Ctx) 1625 } 1626 1627 if err != nil { 1628 ddbErr = d.handleError(err, "DynamoDB RemoveItemAttribute Failed: (UpdateItem to RemoveItemAttribute)") 1629 return fmt.Errorf(ddbErr.ErrorMessage) 1630 } else { 1631 return nil 1632 } 1633 }, &xray.XTraceData{ 1634 Meta: map[string]interface{}{ 1635 "TableName": d.TableName, 1636 "PK": pkValue, 1637 "SK": skValue, 1638 "RemoveExpression": removeExpression, 1639 }, 1640 }) 1641 1642 // remove item attribute successful 1643 return ddbErr 1644 } 1645 1646 func (d *DynamoDB) removeItemAttributeNormal(pkValue string, skValue string, removeExpression string, timeOutDuration *time.Duration) (ddbErr *DynamoDBError) { 1647 if d.cn == nil { 1648 return d.handleError(errors.New("DynamoDB Connection is Required")) 1649 } 1650 1651 if util.LenTrim(d.TableName) <= 0 { 1652 return d.handleError(errors.New("DynamoDB Table Name is Required")) 1653 } 1654 1655 // validate input parameters 1656 if util.LenTrim(d.PKName) <= 0 { 1657 return d.handleError(errors.New("DynamoDB RemoveItemAttribute Failed: " + "PK Name is Required")) 1658 } 1659 1660 if util.LenTrim(pkValue) <= 0 { 1661 return d.handleError(errors.New("DynamoDB RemoveItemAttribute Failed: " + "PK Value is Required")) 1662 } 1663 1664 if util.LenTrim(skValue) > 0 { 1665 if util.LenTrim(d.SKName) <= 0 { 1666 return d.handleError(errors.New("DynamoDB RemoveItemAttribute Failed: " + "SK Name is Required")) 1667 } 1668 } 1669 1670 if util.LenTrim(removeExpression) <= 0 { 1671 return d.handleError(errors.New("DynamoDB RemoveItemAttribute Failed: " + "RemoveExpression is Required")) 1672 } 1673 1674 // define key 1675 m := make(map[string]*dynamodb.AttributeValue) 1676 1677 m[d.PKName] = &dynamodb.AttributeValue{S: aws.String(pkValue)} 1678 1679 if util.LenTrim(skValue) > 0 { 1680 m[d.SKName] = &dynamodb.AttributeValue{S: aws.String(skValue)} 1681 } 1682 1683 // build update item input params 1684 params := &dynamodb.UpdateItemInput{ 1685 TableName: aws.String(d.TableName), 1686 Key: m, 1687 UpdateExpression: aws.String(removeExpression), 1688 ReturnValues: aws.String(dynamodb.ReturnValueAllNew), 1689 } 1690 1691 // record params payload 1692 d.LastExecuteParamsPayload = "RemoveItemAttribute = " + params.String() 1693 1694 // execute dynamodb service 1695 var err error 1696 1697 // create timeout context 1698 if timeOutDuration != nil { 1699 ctx, cancel := context.WithTimeout(context.Background(), *timeOutDuration) 1700 defer cancel() 1701 _, err = d.do_UpdateItem(params, ctx) 1702 } else { 1703 _, err = d.do_UpdateItem(params) 1704 } 1705 1706 if err != nil { 1707 ddbErr = d.handleError(err, "DynamoDB RemoveItemAttribute Failed: (UpdateItem to RemoveItemAttribute)") 1708 } else { 1709 ddbErr = nil 1710 } 1711 1712 // remove item attribute successful 1713 return ddbErr 1714 } 1715 1716 // UpdateItemWithRetry handles dynamodb retries in case action temporarily fails 1717 func (d *DynamoDB) UpdateItemWithRetry(maxRetries uint, 1718 pkValue string, skValue string, 1719 updateExpression string, 1720 conditionExpression string, 1721 expressionAttributeNames map[string]*string, 1722 expressionAttributeValues map[string]*dynamodb.AttributeValue, 1723 timeOutDuration *time.Duration) *DynamoDBError { 1724 if maxRetries > 10 { 1725 maxRetries = 10 1726 } 1727 1728 timeout := 10 * time.Second 1729 1730 if timeOutDuration != nil { 1731 timeout = *timeOutDuration 1732 } 1733 1734 if timeout < 10*time.Second { 1735 timeout = 10 * time.Second 1736 } else if timeout > 30*time.Second { 1737 timeout = 30 * time.Second 1738 } 1739 1740 if err := d.UpdateItem(pkValue, skValue, updateExpression, conditionExpression, expressionAttributeNames, expressionAttributeValues, util.DurationPtr(timeout)); err != nil { 1741 // has error 1742 if maxRetries > 0 { 1743 if err.AllowRetry { 1744 if err.RetryNeedsBackOff { 1745 time.Sleep(500 * time.Millisecond) 1746 } else { 1747 time.Sleep(100 * time.Millisecond) 1748 } 1749 1750 log.Println("UpdateItemWithRetry Failed: " + err.ErrorMessage) 1751 return d.UpdateItemWithRetry(maxRetries-1, pkValue, skValue, updateExpression, conditionExpression, expressionAttributeNames, expressionAttributeValues, util.DurationPtr(timeout)) 1752 } else { 1753 if err.SuppressError { 1754 log.Println("UpdateItemWithRetry DynamoDB Error Suppressed, Returning Error Nil (MaxRetries = " + util.UintToStr(maxRetries) + ")") 1755 return nil 1756 } else { 1757 return &DynamoDBError{ 1758 ErrorMessage: "UpdateItemWithRetry Failed: " + err.ErrorMessage, 1759 SuppressError: false, 1760 AllowRetry: false, 1761 RetryNeedsBackOff: false, 1762 } 1763 } 1764 } 1765 } else { 1766 if err.SuppressError { 1767 log.Println("UpdateItemWithRetry DynamoDB Error Suppressed, Returning Error Nil (MaxRetries = 0)") 1768 return nil 1769 } else { 1770 return &DynamoDBError{ 1771 ErrorMessage: "UpdateItemWithRetry Failed: (MaxRetries = 0) " + err.ErrorMessage, 1772 SuppressError: false, 1773 AllowRetry: false, 1774 RetryNeedsBackOff: false, 1775 } 1776 } 1777 } 1778 } else { 1779 // no error 1780 return nil 1781 } 1782 } 1783 1784 // RemoveItemAttributeWithRetry handles dynamodb retries in case action temporarily fails 1785 func (d *DynamoDB) RemoveItemAttributeWithRetry(maxRetries uint, pkValue string, skValue string, removeExpression string, timeOutDuration *time.Duration) *DynamoDBError { 1786 if maxRetries > 10 { 1787 maxRetries = 10 1788 } 1789 1790 timeout := 10 * time.Second 1791 1792 if timeOutDuration != nil { 1793 timeout = *timeOutDuration 1794 } 1795 1796 if timeout < 10*time.Second { 1797 timeout = 10 * time.Second 1798 } else if timeout > 30*time.Second { 1799 timeout = 30 * time.Second 1800 } 1801 1802 if err := d.RemoveItemAttribute(pkValue, skValue, removeExpression, util.DurationPtr(timeout)); err != nil { 1803 // has error 1804 if maxRetries > 0 { 1805 if err.AllowRetry { 1806 if err.RetryNeedsBackOff { 1807 time.Sleep(500 * time.Millisecond) 1808 } else { 1809 time.Sleep(100 * time.Millisecond) 1810 } 1811 1812 log.Println("RemoveItemAttributeWithRetry Failed: " + err.ErrorMessage) 1813 return d.RemoveItemAttributeWithRetry(maxRetries-1, pkValue, skValue, removeExpression, util.DurationPtr(timeout)) 1814 } else { 1815 if err.SuppressError { 1816 log.Println("RemoveItemAttributeWithRetry DynamoDB Error Suppressed, Returning Error Nil (MaxRetries = " + util.UintToStr(maxRetries) + ")") 1817 return nil 1818 } else { 1819 return &DynamoDBError{ 1820 ErrorMessage: "RemoveItemAttributeWithRetry Failed: " + err.ErrorMessage, 1821 SuppressError: false, 1822 AllowRetry: false, 1823 RetryNeedsBackOff: false, 1824 } 1825 } 1826 } 1827 } else { 1828 if err.SuppressError { 1829 log.Println("RemoveItemAttributeWithRetry DynamoDB Error Suppressed, Returning Error Nil (MaxRetries = 0)") 1830 return nil 1831 } else { 1832 return &DynamoDBError{ 1833 ErrorMessage: "RemoveItemAttributeWithRetry Failed: (MaxRetries = 0) " + err.ErrorMessage, 1834 SuppressError: false, 1835 AllowRetry: false, 1836 RetryNeedsBackOff: false, 1837 } 1838 } 1839 } 1840 } else { 1841 // no error 1842 return nil 1843 } 1844 } 1845 1846 // DeleteItem will delete an existing item from dynamodb table, using primary key values (PK and SK) 1847 // 1848 // important 1849 // 1850 // if dynamodb table is defined as PK and SK together, then to search, MUST use PK and SK together or error will trigger 1851 // 1852 // parameters: 1853 // 1854 // pkValue = required, value of partition key to seek 1855 // skValue = optional, value of sort key to seek; set to blank if value not provided 1856 // timeOutDuration = optional, timeout duration sent via context to scan method; nil if not using timeout duration 1857 func (d *DynamoDB) DeleteItem(pkValue string, skValue string, timeOutDuration *time.Duration) (ddbErr *DynamoDBError) { 1858 if xray.XRayServiceOn() { 1859 return d.deleteItemWithTrace(pkValue, skValue, timeOutDuration) 1860 } else { 1861 return d.deleteItemNormal(pkValue, skValue, timeOutDuration) 1862 } 1863 } 1864 1865 func (d *DynamoDB) deleteItemWithTrace(pkValue string, skValue string, timeOutDuration *time.Duration) (ddbErr *DynamoDBError) { 1866 trace := xray.NewSegment("DynamoDB-DeleteItem", d._parentSegment) 1867 defer trace.Close() 1868 defer func() { 1869 if ddbErr != nil { 1870 _ = trace.Seg.AddError(fmt.Errorf(ddbErr.ErrorMessage)) 1871 } 1872 }() 1873 1874 if d.cn == nil { 1875 ddbErr = d.handleError(errors.New("DynamoDB Connection is Required")) 1876 return ddbErr 1877 } 1878 1879 if util.LenTrim(d.TableName) <= 0 { 1880 ddbErr = d.handleError(errors.New("DynamoDB Table Name is Required")) 1881 return ddbErr 1882 } 1883 1884 if util.LenTrim(d.PKName) <= 0 { 1885 ddbErr = d.handleError(errors.New("DynamoDB DeleteItem Failed: " + "PK Name is Required")) 1886 return ddbErr 1887 } 1888 1889 if util.LenTrim(pkValue) <= 0 { 1890 ddbErr = d.handleError(errors.New("DynamoDB DeleteItem Failed: " + "PK Value is Required")) 1891 return ddbErr 1892 } 1893 1894 if util.LenTrim(skValue) > 0 { 1895 if util.LenTrim(d.SKName) <= 0 { 1896 ddbErr = d.handleError(errors.New("DynamoDB DeleteItem Failed: " + "SK Name is Required")) 1897 return ddbErr 1898 } 1899 } 1900 1901 trace.Capture("DeleteItem", func() error { 1902 m := make(map[string]*dynamodb.AttributeValue) 1903 1904 m[d.PKName] = &dynamodb.AttributeValue{S: aws.String(pkValue)} 1905 1906 if util.LenTrim(skValue) > 0 { 1907 m[d.SKName] = &dynamodb.AttributeValue{S: aws.String(skValue)} 1908 } 1909 1910 params := &dynamodb.DeleteItemInput{ 1911 TableName: aws.String(d.TableName), 1912 Key: m, 1913 } 1914 1915 // record params payload 1916 d.LastExecuteParamsPayload = "DeleteItem = " + params.String() 1917 1918 var err error 1919 1920 subTrace := trace.NewSubSegment("DeleteItem_Do") 1921 defer subTrace.Close() 1922 1923 if timeOutDuration != nil { 1924 ctx, cancel := context.WithTimeout(subTrace.Ctx, *timeOutDuration) 1925 defer cancel() 1926 _, err = d.do_DeleteItem(params, ctx) 1927 } else { 1928 _, err = d.do_DeleteItem(params, subTrace.Ctx) 1929 } 1930 1931 if err != nil { 1932 ddbErr = d.handleError(err, "DynamoDB DeleteItem Failed: (DeleteItem)") 1933 return fmt.Errorf(ddbErr.ErrorMessage) 1934 } else { 1935 return nil 1936 } 1937 }, &xray.XTraceData{ 1938 Meta: map[string]interface{}{ 1939 "TableName": d.TableName, 1940 "PK": pkValue, 1941 "SK": skValue, 1942 }, 1943 }) 1944 1945 // delete item was successful 1946 return ddbErr 1947 } 1948 1949 func (d *DynamoDB) deleteItemNormal(pkValue string, skValue string, timeOutDuration *time.Duration) (ddbErr *DynamoDBError) { 1950 if d.cn == nil { 1951 return d.handleError(errors.New("DynamoDB Connection is Required")) 1952 } 1953 1954 if util.LenTrim(d.TableName) <= 0 { 1955 return d.handleError(errors.New("DynamoDB Table Name is Required")) 1956 } 1957 1958 if util.LenTrim(d.PKName) <= 0 { 1959 return d.handleError(errors.New("DynamoDB DeleteItem Failed: " + "PK Name is Required")) 1960 } 1961 1962 if util.LenTrim(pkValue) <= 0 { 1963 return d.handleError(errors.New("DynamoDB DeleteItem Failed: " + "PK Value is Required")) 1964 } 1965 1966 if util.LenTrim(skValue) > 0 { 1967 if util.LenTrim(d.SKName) <= 0 { 1968 return d.handleError(errors.New("DynamoDB DeleteItem Failed: " + "SK Name is Required")) 1969 } 1970 } 1971 1972 m := make(map[string]*dynamodb.AttributeValue) 1973 1974 m[d.PKName] = &dynamodb.AttributeValue{S: aws.String(pkValue)} 1975 1976 if util.LenTrim(skValue) > 0 { 1977 m[d.SKName] = &dynamodb.AttributeValue{S: aws.String(skValue)} 1978 } 1979 1980 params := &dynamodb.DeleteItemInput{ 1981 TableName: aws.String(d.TableName), 1982 Key: m, 1983 } 1984 1985 // record params payload 1986 d.LastExecuteParamsPayload = "DeleteItem = " + params.String() 1987 1988 var err error 1989 1990 if timeOutDuration != nil { 1991 ctx, cancel := context.WithTimeout(context.Background(), *timeOutDuration) 1992 defer cancel() 1993 _, err = d.do_DeleteItem(params, ctx) 1994 } else { 1995 _, err = d.do_DeleteItem(params) 1996 } 1997 1998 if err != nil { 1999 ddbErr = d.handleError(err, "DynamoDB DeleteItem Failed: (DeleteItem)") 2000 } else { 2001 ddbErr = nil 2002 } 2003 2004 // delete item was successful 2005 return ddbErr 2006 } 2007 2008 // DeleteItemWithRetry handles dynamodb retries in case action temporarily fails 2009 func (d *DynamoDB) DeleteItemWithRetry(maxRetries uint, pkValue string, skValue string, timeOutDuration *time.Duration) *DynamoDBError { 2010 if maxRetries > 10 { 2011 maxRetries = 10 2012 } 2013 2014 timeout := 5 * time.Second 2015 2016 if timeOutDuration != nil { 2017 timeout = *timeOutDuration 2018 } 2019 2020 if timeout < 5*time.Second { 2021 timeout = 5 * time.Second 2022 } else if timeout > 15*time.Second { 2023 timeout = 15 * time.Second 2024 } 2025 2026 if err := d.DeleteItem(pkValue, skValue, util.DurationPtr(timeout)); err != nil { 2027 // has error 2028 if maxRetries > 0 { 2029 if err.AllowRetry { 2030 if err.RetryNeedsBackOff { 2031 time.Sleep(500 * time.Millisecond) 2032 } else { 2033 time.Sleep(100 * time.Millisecond) 2034 } 2035 2036 log.Println("DeleteItemWithRetry Failed: " + err.ErrorMessage) 2037 return d.DeleteItemWithRetry(maxRetries-1, pkValue, skValue, util.DurationPtr(timeout)) 2038 } else { 2039 if err.SuppressError { 2040 log.Println("DeleteItemWithRetry DynamoDB Error Suppressed, Returning Error Nil (MaxRetries = " + util.UintToStr(maxRetries) + ")") 2041 return nil 2042 } else { 2043 return &DynamoDBError{ 2044 ErrorMessage: "DeleteItemWithRetry Failed: " + err.ErrorMessage, 2045 SuppressError: false, 2046 AllowRetry: false, 2047 RetryNeedsBackOff: false, 2048 } 2049 } 2050 } 2051 } else { 2052 if err.SuppressError { 2053 log.Println("DeleteItemWithRetry DynamoDB Error Suppressed, Returning Error Nil (MaxRetries = 0)") 2054 return nil 2055 } else { 2056 return &DynamoDBError{ 2057 ErrorMessage: "DeleteItemWithRetry Failed: (MaxRetries = 0) " + err.ErrorMessage, 2058 SuppressError: false, 2059 AllowRetry: false, 2060 RetryNeedsBackOff: false, 2061 } 2062 } 2063 } 2064 } else { 2065 // no error 2066 return nil 2067 } 2068 } 2069 2070 // GetItem will find an existing item from dynamodb table 2071 // 2072 // important 2073 // 2074 // if dynamodb table is defined as PK and SK together, then to search, MUST use PK and SK together or error will trigger 2075 // 2076 // warning 2077 // 2078 // projectedAttributes = if specified, must include PartitionKey (Hash key) typically "PK" as the first attribute in projected attributes 2079 // 2080 // parameters: 2081 // 2082 // resultItemPtr = required, pointer to item object for return value to unmarshal into; if projected attributes less than struct fields, unmatched is defaulted 2083 // a) MUST BE STRUCT OBJECT; NEVER A SLICE 2084 // pkValue = required, value of partition key to seek 2085 // skValue = optional, value of sort key to seek; set to blank if value not provided 2086 // timeOutDuration = optional, timeout duration sent via context to scan method; nil if not using timeout duration 2087 // consistentRead = optional, scan uses consistent read or eventual consistent read, default is eventual consistent read 2088 // projectedAttributes = optional; ATTRIBUTES ARE CASE SENSITIVE; variadic list of attribute names that this query will project into result items; 2089 // attribute names must match struct field name or struct tag's json / dynamodbav tag values, 2090 // if specified, must include PartitionKey (Hash key) typically "PK" as the first attribute in projected attributes 2091 // 2092 // notes: 2093 // 2094 // item struct tags 2095 // use `json:"" dynamodbav:""` 2096 // json = sets the name used in json 2097 // dynamodbav = sets the name used in dynamodb 2098 // reference child element 2099 // if struct has field with complex type (another struct), to reference it in code, use the parent struct field dot child field notation 2100 // Info in parent struct with struct tag as info; to reach child element: info.xyz 2101 func (d *DynamoDB) GetItem(resultItemPtr interface{}, 2102 pkValue string, skValue string, 2103 timeOutDuration *time.Duration, consistentRead *bool, projectedAttributes ...string) (ddbErr *DynamoDBError) { 2104 if xray.XRayServiceOn() { 2105 return d.getItemWithTrace(resultItemPtr, pkValue, skValue, timeOutDuration, consistentRead, projectedAttributes...) 2106 } else { 2107 return d.getItemNormal(resultItemPtr, pkValue, skValue, timeOutDuration, consistentRead, projectedAttributes...) 2108 } 2109 } 2110 2111 func (d *DynamoDB) getItemWithTrace(resultItemPtr interface{}, 2112 pkValue string, skValue string, 2113 timeOutDuration *time.Duration, consistentRead *bool, projectedAttributes ...string) (ddbErr *DynamoDBError) { 2114 trace := xray.NewSegment("DynamoDB-GetItem", d._parentSegment) 2115 defer trace.Close() 2116 defer func() { 2117 if ddbErr != nil { 2118 _ = trace.Seg.AddError(fmt.Errorf(ddbErr.ErrorMessage)) 2119 } 2120 }() 2121 2122 if d.cn == nil { 2123 ddbErr = d.handleError(errors.New("DynamoDB Connection is Required")) 2124 return ddbErr 2125 } 2126 2127 if util.LenTrim(d.TableName) <= 0 { 2128 ddbErr = d.handleError(errors.New("DynamoDB Table Name is Required")) 2129 return ddbErr 2130 } 2131 2132 // validate input parameters 2133 if resultItemPtr == nil { 2134 ddbErr = d.handleError(errors.New("DynamoDB GetItem Failed: " + "ResultItemPtr Must Initialize First")) 2135 return ddbErr 2136 } 2137 2138 if util.LenTrim(d.PKName) <= 0 { 2139 ddbErr = d.handleError(errors.New("DynamoDB GetItem Failed: " + "PK Name is Required")) 2140 return ddbErr 2141 } 2142 2143 if util.LenTrim(pkValue) <= 0 { 2144 ddbErr = d.handleError(errors.New("DynamoDB GetItem Failed: " + "PK Value is Required")) 2145 return ddbErr 2146 } 2147 2148 if util.LenTrim(skValue) > 0 { 2149 if util.LenTrim(d.SKName) <= 0 { 2150 ddbErr = d.handleError(errors.New("DynamoDB GetItem Failed: " + "SK Name is Required")) 2151 return ddbErr 2152 } 2153 } 2154 2155 trace.Capture("GetItem", func() error { 2156 // define key filter 2157 m := make(map[string]*dynamodb.AttributeValue) 2158 2159 m[d.PKName] = &dynamodb.AttributeValue{S: aws.String(pkValue)} 2160 2161 if util.LenTrim(skValue) > 0 { 2162 m[d.SKName] = &dynamodb.AttributeValue{S: aws.String(skValue)} 2163 } 2164 2165 // define projected attributes 2166 var proj expression.ProjectionBuilder 2167 projSet := false 2168 2169 if len(projectedAttributes) > 0 { 2170 // compose projected attributes if specified 2171 firstProjectedAttribute := expression.Name(projectedAttributes[0]) 2172 moreProjectedAttributes := []expression.NameBuilder{} 2173 2174 if len(projectedAttributes) > 1 { 2175 firstAttribute := true 2176 2177 for _, v := range projectedAttributes { 2178 if !firstAttribute { 2179 moreProjectedAttributes = append(moreProjectedAttributes, expression.Name(v)) 2180 } else { 2181 firstAttribute = false 2182 } 2183 } 2184 } 2185 2186 if len(moreProjectedAttributes) > 0 { 2187 proj = expression.NamesList(firstProjectedAttribute, moreProjectedAttributes...) 2188 } else { 2189 proj = expression.NamesList(firstProjectedAttribute) 2190 } 2191 2192 projSet = true 2193 } 2194 2195 // compose filter expression and projection if applicable 2196 var expr expression.Expression 2197 var err error 2198 2199 if projSet { 2200 if expr, err = expression.NewBuilder().WithProjection(proj).Build(); err != nil { 2201 ddbErr = d.handleError(err, "DynamoDB GetItem Failed: (GetItem)") 2202 return fmt.Errorf(ddbErr.ErrorMessage) 2203 } 2204 } 2205 2206 // set params 2207 params := &dynamodb.GetItemInput{ 2208 TableName: aws.String(d.TableName), 2209 Key: m, 2210 } 2211 2212 if projSet { 2213 params.ProjectionExpression = expr.Projection() 2214 params.ExpressionAttributeNames = expr.Names() 2215 } 2216 2217 if consistentRead != nil { 2218 if *consistentRead { 2219 params.ConsistentRead = consistentRead 2220 } 2221 } 2222 2223 // record params payload 2224 d.LastExecuteParamsPayload = "GetItem = " + params.String() 2225 2226 // execute get item action 2227 var result *dynamodb.GetItemOutput 2228 2229 subTrace := trace.NewSubSegment("GetItem_Do") 2230 defer subTrace.Close() 2231 2232 if timeOutDuration != nil { 2233 ctx, cancel := context.WithTimeout(subTrace.Ctx, *timeOutDuration) 2234 defer cancel() 2235 result, err = d.do_GetItem(params, ctx) 2236 } else { 2237 result, err = d.do_GetItem(params, subTrace.Ctx) 2238 } 2239 2240 // evaluate result 2241 if err != nil { 2242 ddbErr = d.handleError(err, "DynamoDB GetItem Failed: (GetItem)") 2243 return fmt.Errorf(ddbErr.ErrorMessage) 2244 } 2245 2246 if result == nil { 2247 ddbErr = d.handleError(errors.New("DynamoDB GetItem Failed: " + "Result Object Nil")) 2248 return fmt.Errorf(ddbErr.ErrorMessage) 2249 } 2250 2251 if err = dynamodbattribute.UnmarshalMap(result.Item, resultItemPtr); err != nil { 2252 ddbErr = d.handleError(err, "DynamoDB GetItem Failed: (Unmarshal)") 2253 return fmt.Errorf(ddbErr.ErrorMessage) 2254 } else { 2255 return nil 2256 } 2257 }, &xray.XTraceData{ 2258 Meta: map[string]interface{}{ 2259 "TableName": d.TableName, 2260 "PK": pkValue, 2261 "SK": skValue, 2262 }, 2263 }) 2264 2265 // get item was successful 2266 return ddbErr 2267 } 2268 2269 func (d *DynamoDB) getItemNormal(resultItemPtr interface{}, 2270 pkValue string, skValue string, 2271 timeOutDuration *time.Duration, consistentRead *bool, projectedAttributes ...string) (ddbErr *DynamoDBError) { 2272 if d.cn == nil { 2273 return d.handleError(errors.New("DynamoDB Connection is Required")) 2274 } 2275 2276 if util.LenTrim(d.TableName) <= 0 { 2277 return d.handleError(errors.New("DynamoDB Table Name is Required")) 2278 } 2279 2280 // validate input parameters 2281 if resultItemPtr == nil { 2282 return d.handleError(errors.New("DynamoDB GetItem Failed: " + "ResultItemPtr Must Initialize First")) 2283 } 2284 2285 if util.LenTrim(d.PKName) <= 0 { 2286 return d.handleError(errors.New("DynamoDB GetItem Failed: " + "PK Name is Required")) 2287 } 2288 2289 if util.LenTrim(pkValue) <= 0 { 2290 return d.handleError(errors.New("DynamoDB GetItem Failed: " + "PK Value is Required")) 2291 } 2292 2293 if util.LenTrim(skValue) > 0 { 2294 if util.LenTrim(d.SKName) <= 0 { 2295 return d.handleError(errors.New("DynamoDB GetItem Failed: " + "SK Name is Required")) 2296 } 2297 } 2298 2299 // define key filter 2300 m := make(map[string]*dynamodb.AttributeValue) 2301 2302 m[d.PKName] = &dynamodb.AttributeValue{S: aws.String(pkValue)} 2303 2304 if util.LenTrim(skValue) > 0 { 2305 m[d.SKName] = &dynamodb.AttributeValue{S: aws.String(skValue)} 2306 } 2307 2308 // define projected attributes 2309 var proj expression.ProjectionBuilder 2310 projSet := false 2311 2312 if len(projectedAttributes) > 0 { 2313 // compose projected attributes if specified 2314 firstProjectedAttribute := expression.Name(projectedAttributes[0]) 2315 moreProjectedAttributes := []expression.NameBuilder{} 2316 2317 if len(projectedAttributes) > 1 { 2318 firstAttribute := true 2319 2320 for _, v := range projectedAttributes { 2321 if !firstAttribute { 2322 moreProjectedAttributes = append(moreProjectedAttributes, expression.Name(v)) 2323 } else { 2324 firstAttribute = false 2325 } 2326 } 2327 } 2328 2329 if len(moreProjectedAttributes) > 0 { 2330 proj = expression.NamesList(firstProjectedAttribute, moreProjectedAttributes...) 2331 } else { 2332 proj = expression.NamesList(firstProjectedAttribute) 2333 } 2334 2335 projSet = true 2336 } 2337 2338 // compose filter expression and projection if applicable 2339 var expr expression.Expression 2340 var err error 2341 2342 if projSet { 2343 if expr, err = expression.NewBuilder().WithProjection(proj).Build(); err != nil { 2344 return d.handleError(err, "DynamoDB GetItem Failed: (GetItem)") 2345 } 2346 } 2347 2348 // set params 2349 params := &dynamodb.GetItemInput{ 2350 TableName: aws.String(d.TableName), 2351 Key: m, 2352 } 2353 2354 if projSet { 2355 params.ProjectionExpression = expr.Projection() 2356 params.ExpressionAttributeNames = expr.Names() 2357 } 2358 2359 if consistentRead != nil { 2360 if *consistentRead { 2361 params.ConsistentRead = consistentRead 2362 } 2363 } 2364 2365 // record params payload 2366 d.LastExecuteParamsPayload = "GetItem = " + params.String() 2367 2368 // execute get item action 2369 var result *dynamodb.GetItemOutput 2370 2371 if timeOutDuration != nil { 2372 ctx, cancel := context.WithTimeout(context.Background(), *timeOutDuration) 2373 defer cancel() 2374 result, err = d.do_GetItem(params, ctx) 2375 } else { 2376 result, err = d.do_GetItem(params) 2377 } 2378 2379 // evaluate result 2380 if err != nil { 2381 return d.handleError(err, "DynamoDB GetItem Failed: (GetItem)") 2382 } 2383 2384 if result == nil { 2385 return d.handleError(errors.New("DynamoDB GetItem Failed: " + "Result Object Nil")) 2386 } 2387 2388 if err = dynamodbattribute.UnmarshalMap(result.Item, resultItemPtr); err != nil { 2389 ddbErr = d.handleError(err, "DynamoDB GetItem Failed: (Unmarshal)") 2390 } else { 2391 ddbErr = nil 2392 } 2393 2394 // get item was successful 2395 return ddbErr 2396 } 2397 2398 // GetItemWithRetry handles dynamodb retries in case action temporarily fails 2399 // 2400 // warning 2401 // 2402 // projectedAttributes = if specified, must include PartitionKey (Hash key) typically "PK" as the first attribute in projected attributes 2403 func (d *DynamoDB) GetItemWithRetry(maxRetries uint, 2404 resultItemPtr interface{}, pkValue string, skValue string, 2405 timeOutDuration *time.Duration, consistentRead *bool, projectedAttributes ...string) *DynamoDBError { 2406 if maxRetries > 10 { 2407 maxRetries = 10 2408 } 2409 2410 timeout := 5 * time.Second 2411 2412 if timeOutDuration != nil { 2413 timeout = *timeOutDuration 2414 } 2415 2416 if timeout < 5*time.Second { 2417 timeout = 5 * time.Second 2418 } else if timeout > 15*time.Second { 2419 timeout = 15 * time.Second 2420 } 2421 2422 if err := d.GetItem(resultItemPtr, pkValue, skValue, util.DurationPtr(timeout), consistentRead, projectedAttributes...); err != nil { 2423 // has error 2424 if maxRetries > 0 { 2425 if err.AllowRetry { 2426 if err.RetryNeedsBackOff { 2427 time.Sleep(500 * time.Millisecond) 2428 } else { 2429 time.Sleep(100 * time.Millisecond) 2430 } 2431 2432 log.Println("GetItemWithRetry Failed: " + err.ErrorMessage) 2433 return d.GetItemWithRetry(maxRetries-1, resultItemPtr, pkValue, skValue, util.DurationPtr(timeout), consistentRead, projectedAttributes...) 2434 } else { 2435 if err.SuppressError { 2436 log.Println("GetItemWithRetry DynamoDB Error Suppressed, Returning Error Nil (MaxRetries = " + util.UintToStr(maxRetries) + ")") 2437 return nil 2438 } else { 2439 return &DynamoDBError{ 2440 ErrorMessage: "GetItemWithRetry Failed: " + err.ErrorMessage, 2441 SuppressError: false, 2442 AllowRetry: false, 2443 RetryNeedsBackOff: false, 2444 } 2445 } 2446 } 2447 } else { 2448 if err.SuppressError { 2449 log.Println("GetItemWithRetry DynamoDB Error Suppressed, Returning Error Nil (MaxRetries = 0)") 2450 return nil 2451 } else { 2452 return &DynamoDBError{ 2453 ErrorMessage: "GetItemWithRetry Failed: (MaxRetries = 0) " + err.ErrorMessage, 2454 SuppressError: false, 2455 AllowRetry: false, 2456 RetryNeedsBackOff: false, 2457 } 2458 } 2459 } 2460 } else { 2461 // no error 2462 return nil 2463 } 2464 } 2465 2466 // QueryPaginationDataWithRetry returns slice of ExclusiveStartKeys, 2467 // with first element always a nil to represent no exclusiveStartKey, 2468 // and each subsequent element starts from page 2 with its own exclusiveStartKey. 2469 // 2470 // if slice is nil or zero element, then it also indicates single page, 2471 // same as if slice is single element with nil indicating single page. 2472 // 2473 // Caller can use this info to pre-build the pagination buttons, so that clicking page 1 simply query using no exclusiveStartKey, 2474 // where as query page 2 uses the exclusiveStartKey from element 1 of the slice, and so on. 2475 func (d *DynamoDB) QueryPaginationDataWithRetry( 2476 maxRetries uint, 2477 timeOutDuration *time.Duration, 2478 indexName *string, 2479 itemsPerPage int64, 2480 keyConditionExpression string, 2481 expressionAttributeNames map[string]*string, 2482 expressionAttributeValues map[string]*dynamodb.AttributeValue) (paginationData []map[string]*dynamodb.AttributeValue, ddbErr *DynamoDBError) { 2483 2484 if maxRetries > 10 { 2485 maxRetries = 10 2486 } 2487 2488 timeout := 5 * time.Second 2489 2490 if timeOutDuration != nil { 2491 timeout = *timeOutDuration 2492 } 2493 2494 if timeout < 5*time.Second { 2495 timeout = 5 * time.Second 2496 } else if timeout > 15*time.Second { 2497 timeout = 15 * time.Second 2498 } 2499 2500 if paginationData, ddbErr = d.queryPaginationDataWrapper(util.DurationPtr(timeout), indexName, itemsPerPage, keyConditionExpression, expressionAttributeNames, expressionAttributeValues); ddbErr != nil { 2501 // has error 2502 if maxRetries > 0 { 2503 if ddbErr.AllowRetry { 2504 if ddbErr.RetryNeedsBackOff { 2505 time.Sleep(500 * time.Millisecond) 2506 } else { 2507 time.Sleep(100 * time.Millisecond) 2508 } 2509 2510 log.Println("QueryPaginationDataWithRetry Failed: " + ddbErr.ErrorMessage) 2511 return d.QueryPaginationDataWithRetry(maxRetries-1, util.DurationPtr(timeout), indexName, itemsPerPage, keyConditionExpression, expressionAttributeNames, expressionAttributeValues) 2512 } else { 2513 if ddbErr.SuppressError { 2514 log.Println("QueryPaginationDataWithRetry DynamoDB Error Suppressed, Returning Error Nil (MaxRetries = " + util.UintToStr(maxRetries) + ")") 2515 return nil, nil 2516 } else { 2517 return nil, &DynamoDBError{ 2518 ErrorMessage: "QueryPaginationDataWithRetry Failed: " + ddbErr.ErrorMessage, 2519 SuppressError: false, 2520 AllowRetry: false, 2521 RetryNeedsBackOff: false, 2522 } 2523 } 2524 } 2525 } else { 2526 if ddbErr.SuppressError { 2527 log.Println("QueryPaginationDataWithRetry DynamoDB Error Suppressed, Returning Error Nil (MaxRetries = 0)") 2528 return nil, nil 2529 } else { 2530 return nil, &DynamoDBError{ 2531 ErrorMessage: "QueryPaginationDataWithRetry Failed: (MaxRetries = 0) " + ddbErr.ErrorMessage, 2532 SuppressError: false, 2533 AllowRetry: false, 2534 RetryNeedsBackOff: false, 2535 } 2536 } 2537 } 2538 } else { 2539 // no error 2540 if paginationData == nil { 2541 paginationData = make([]map[string]*dynamodb.AttributeValue, 1) 2542 } else { 2543 paginationData = append([]map[string]*dynamodb.AttributeValue{nil}, paginationData...) 2544 } 2545 2546 return paginationData, nil 2547 } 2548 } 2549 2550 func (d *DynamoDB) queryPaginationDataWrapper( 2551 timeOutDuration *time.Duration, 2552 indexName *string, 2553 itemsPerPage int64, 2554 keyConditionExpression string, 2555 expressionAttributeNames map[string]*string, 2556 expressionAttributeValues map[string]*dynamodb.AttributeValue) (paginationData []map[string]*dynamodb.AttributeValue, ddbErr *DynamoDBError) { 2557 2558 if xray.XRayServiceOn() { 2559 return d.queryPaginationDataWithTrace(timeOutDuration, indexName, itemsPerPage, keyConditionExpression, expressionAttributeNames, expressionAttributeValues) 2560 } else { 2561 return d.queryPaginationDataNormal(timeOutDuration, indexName, itemsPerPage, keyConditionExpression, expressionAttributeNames, expressionAttributeValues) 2562 } 2563 } 2564 2565 func (d *DynamoDB) queryPaginationDataWithTrace( 2566 timeOutDuration *time.Duration, 2567 indexName *string, 2568 itemsPerPage int64, 2569 keyConditionExpression string, 2570 expressionAttributeNames map[string]*string, 2571 expressionAttributeValues map[string]*dynamodb.AttributeValue) (paginationData []map[string]*dynamodb.AttributeValue, ddbErr *DynamoDBError) { 2572 2573 trace := xray.NewSegment("DynamoDB-QueryPaginationDataWithTrace", d._parentSegment) 2574 defer trace.Close() 2575 defer func() { 2576 if ddbErr != nil { 2577 _ = trace.Seg.AddError(fmt.Errorf(ddbErr.ErrorMessage)) 2578 } 2579 }() 2580 2581 if d.cn == nil { 2582 ddbErr = d.handleError(errors.New("QueryPaginationDataWithTrace Failed: DynamoDB Connection is Required")) 2583 return nil, ddbErr 2584 } 2585 2586 if util.LenTrim(d.TableName) <= 0 { 2587 ddbErr = d.handleError(errors.New("QueryPaginationDataWithTrace Failed: DynamoDB Table Name is Required")) 2588 return nil, ddbErr 2589 } 2590 2591 // validate additional input parameters 2592 if util.LenTrim(keyConditionExpression) <= 0 { 2593 ddbErr = d.handleError(errors.New("QueryPaginationDataWithTrace Failed: KeyConditionExpress is Required")) 2594 return nil, ddbErr 2595 } 2596 2597 if expressionAttributeValues == nil { 2598 ddbErr = d.handleError(errors.New("QueryPaginationDataWithTrace Failed: ExpressionAttributeValues is Required")) 2599 return nil, ddbErr 2600 } 2601 2602 trace.Capture("QueryPaginationDataWithTrace", func() error { 2603 // compose filter expression and projection if applicable 2604 expr, err := expression.NewBuilder().WithProjection(expression.NamesList(expression.Name("PK"))).Build() 2605 2606 if err != nil { 2607 ddbErr = d.handleError(err, "QueryPaginationDataWithTrace Failed: (Filter/Projection Expression Build)") 2608 return fmt.Errorf(ddbErr.ErrorMessage) 2609 } 2610 2611 // build query input params 2612 params := &dynamodb.QueryInput{ 2613 TableName: aws.String(d.TableName), 2614 KeyConditionExpression: aws.String(keyConditionExpression), 2615 ExpressionAttributeValues: expressionAttributeValues, 2616 } 2617 2618 if expressionAttributeNames != nil { 2619 params.ExpressionAttributeNames = expressionAttributeNames 2620 } 2621 2622 params.FilterExpression = expr.Filter() 2623 2624 if params.ExpressionAttributeNames == nil { 2625 params.ExpressionAttributeNames = make(map[string]*string) 2626 } 2627 2628 for k, v := range expr.Names() { 2629 params.ExpressionAttributeNames[k] = v 2630 } 2631 2632 for k, v := range expr.Values() { 2633 params.ExpressionAttributeValues[k] = v 2634 } 2635 2636 params.ProjectionExpression = expr.Projection() 2637 2638 if params.ExpressionAttributeNames == nil { 2639 params.ExpressionAttributeNames = expr.Names() 2640 } else { 2641 for k1, v1 := range expr.Names() { 2642 params.ExpressionAttributeNames[k1] = v1 2643 } 2644 } 2645 2646 if indexName != nil && util.LenTrim(*indexName) > 0 { 2647 params.IndexName = indexName 2648 } 2649 2650 params.Limit = aws.Int64(itemsPerPage) 2651 2652 // record params payload 2653 d.LastExecuteParamsPayload = "QueryPaginationDataWithTrace = " + params.String() 2654 2655 subTrace := trace.NewSubSegment("QueryPaginationDataWithTrace_Do") 2656 defer subTrace.Close() 2657 2658 if timeOutDuration != nil { 2659 ctx, cancel := context.WithTimeout(subTrace.Ctx, *timeOutDuration) 2660 defer cancel() 2661 paginationData, err = d.do_Query_Pagination_Data(params, ctx) 2662 } else { 2663 paginationData, err = d.do_Query_Pagination_Data(params, subTrace.Ctx) 2664 } 2665 2666 if err != nil { 2667 ddbErr = d.handleError(err, "QueryPaginationDataWithTrace Failed: (QueryPaginationDataWithTrace)") 2668 return fmt.Errorf(ddbErr.ErrorMessage) 2669 } 2670 2671 return nil 2672 2673 }, &xray.XTraceData{ 2674 Meta: map[string]interface{}{ 2675 "TableName": d.TableName, 2676 "IndexName": aws.StringValue(indexName), 2677 "KeyConditionExpression": keyConditionExpression, 2678 "ExpressionAttributeNames": expressionAttributeNames, 2679 "ExpressionAttributeValues": expressionAttributeValues, 2680 }, 2681 }) 2682 2683 // query items successful 2684 return paginationData, ddbErr 2685 } 2686 2687 func (d *DynamoDB) queryPaginationDataNormal( 2688 timeOutDuration *time.Duration, 2689 indexName *string, 2690 itemsPerPage int64, 2691 keyConditionExpression string, 2692 expressionAttributeNames map[string]*string, 2693 expressionAttributeValues map[string]*dynamodb.AttributeValue) (paginationData []map[string]*dynamodb.AttributeValue, ddbErr *DynamoDBError) { 2694 2695 if d.cn == nil { 2696 return nil, d.handleError(errors.New("QueryPaginationDataNormal Failed: DynamoDB Connection is Required")) 2697 } 2698 2699 if util.LenTrim(d.TableName) <= 0 { 2700 return nil, d.handleError(errors.New("QueryPaginationDataNormal Failed: DynamoDB Table Name is Required")) 2701 } 2702 2703 // validate additional input parameters 2704 if util.LenTrim(keyConditionExpression) <= 0 { 2705 return nil, d.handleError(errors.New("QueryPaginationDataNormal Failed: KeyConditionExpress is Required")) 2706 } 2707 2708 if expressionAttributeValues == nil { 2709 return nil, d.handleError(errors.New("QueryPaginationDataNormal Failed: ExpressionAttributeValues is Required")) 2710 } 2711 2712 // compose filter expression and projection if applicable 2713 expr, err := expression.NewBuilder().WithProjection(expression.NamesList(expression.Name("PK"))).Build() 2714 2715 if err != nil { 2716 return nil, d.handleError(err, "QueryPaginationDataNormal Failed: (Filter/Projection Expression Build)") 2717 } 2718 2719 // build query input params 2720 params := &dynamodb.QueryInput{ 2721 TableName: aws.String(d.TableName), 2722 KeyConditionExpression: aws.String(keyConditionExpression), 2723 ExpressionAttributeValues: expressionAttributeValues, 2724 } 2725 2726 if expressionAttributeNames != nil { 2727 params.ExpressionAttributeNames = expressionAttributeNames 2728 } 2729 2730 params.FilterExpression = expr.Filter() 2731 2732 if params.ExpressionAttributeNames == nil { 2733 params.ExpressionAttributeNames = make(map[string]*string) 2734 } 2735 2736 for k, v := range expr.Names() { 2737 params.ExpressionAttributeNames[k] = v 2738 } 2739 2740 for k, v := range expr.Values() { 2741 params.ExpressionAttributeValues[k] = v 2742 } 2743 2744 params.ProjectionExpression = expr.Projection() 2745 2746 if params.ExpressionAttributeNames == nil { 2747 params.ExpressionAttributeNames = expr.Names() 2748 } else { 2749 for k1, v1 := range expr.Names() { 2750 params.ExpressionAttributeNames[k1] = v1 2751 } 2752 } 2753 2754 if indexName != nil && util.LenTrim(*indexName) > 0 { 2755 params.IndexName = indexName 2756 } 2757 2758 params.Limit = aws.Int64(itemsPerPage) 2759 2760 // record params payload 2761 d.LastExecuteParamsPayload = "QueryPaginationDataNormal = " + params.String() 2762 2763 if timeOutDuration != nil { 2764 ctx, cancel := context.WithTimeout(context.Background(), *timeOutDuration) 2765 defer cancel() 2766 paginationData, err = d.do_Query_Pagination_Data(params, ctx) 2767 } else { 2768 paginationData, err = d.do_Query_Pagination_Data(params) 2769 } 2770 2771 if err != nil { 2772 return nil, d.handleError(err, "QueryPaginationDataNormal Failed: (QueryPaginationDataNormal)") 2773 } 2774 2775 if paginationData == nil { 2776 return nil, d.handleError(err, "QueryPaginationDataNormal Failed: (QueryPaginationDataNormal)") 2777 } 2778 2779 // query pagination data successful 2780 return paginationData, nil 2781 } 2782 2783 // QueryItems will query dynamodb items in given table using primary key (PK, SK for example), or one of Global/Local Secondary Keys (indexName must be defined if using GSI) 2784 // To query against non-key attributes, use Scan (bad for performance however) 2785 // QueryItems requires using Key attributes, and limited to TWO key attributes in condition maximum; 2786 // 2787 // important 2788 // 2789 // if dynamodb table is defined as PK and SK together, then to search without GSI/LSI, MUST use PK and SK together or error will trigger 2790 // 2791 // warning 2792 // 2793 // projectedAttributes = if specified, must include PartitionKey (Hash key) typically "PK" as the first attribute in projected attributes 2794 // 2795 // parameters: 2796 // 2797 // resultItemsPtr = required, pointer to items list struct to contain queried result; i.e. []Item{} where Item is struct; if projected attributes less than struct fields, unmatched is defaulted 2798 // timeOutDuration = optional, timeout duration sent via context to scan method; nil if not using timeout duration 2799 // consistentRead = optional, scan uses consistent read or eventual consistent read, default is eventual consistent read 2800 // indexName = optional, global secondary index or local secondary index name to help in query operation 2801 // pageLimit = optional, scan page limit if set, this limits number of items examined per page during scan operation, allowing scan to work better for RCU 2802 // pagedQuery = optional, indicates if query is page based or not; if true, query will be performed via pages, this helps overcome 1 MB limit of each query result 2803 // pagedQueryPageCountLimit = optional, indicates how many pages to query during paged query action 2804 // exclusiveStartKey = optional, if using pagedQuery and starting the query from prior results 2805 // 2806 // keyConditionExpression = required, ATTRIBUTES ARE CASE SENSITIVE; either the primary key (PK SK for example) or global secondary index (SK Data for example) or another secondary index (secondary index must be named) 2807 // Usage Syntax: 2808 // 1) Max 2 Attribute Fields 2809 // 2) First Field must be Partition Key (Must Evaluate to True or False) 2810 // a) = ONLY 2811 // 3) Second Field is Sort Key (May Evaluate to True or False or Range) 2812 // a) =, <, <=, >, >=, BETWEEN, begins_with() 2813 // 4) Combine Two Fields with AND 2814 // 5) When Attribute Name is Reserved Keyword, Use ExpressionAttributeNames to Define #xyz to Alias 2815 // a) Use the #xyz in the KeyConditionExpression such as #yr = :year (:year is Defined ExpressionAttributeValue) 2816 // 6) Example 2817 // a) partitionKeyName = :partitionKeyVal 2818 // b) partitionKeyName = :partitionKeyVal AND sortKeyName = :sortKeyVal 2819 // c) #yr = :year 2820 // 7) If Using GSI / Local Index 2821 // a) When Using, Must Specify the IndexName 2822 // b) First Field is the GSI's Partition Key, such as SK (Evals to True/False), While Second Field is the GSI's SortKey (Range) 2823 // 2824 // expressionAttributeNames = optional, ATTRIBUTES ARE CASE SENSITIVE; set nil if not used, must define for attribute names that are reserved keywords such as year, data etc. using #xyz 2825 // Usage Syntax: 2826 // 1) map[string]*string: where string is the #xyz, and *string is the original xyz attribute name 2827 // a) map[string]*string { "#xyz": aws.String("Xyz"), } 2828 // 2) Add to Map 2829 // a) m := make(map[string]*string) 2830 // b) m["#xyz"] = aws.String("Xyz") 2831 // 2832 // expressionAttributeValues = required, ATTRIBUTES ARE CASE SENSITIVE; sets the value token and value actual to be used within the keyConditionExpression; this sets both compare token and compare value 2833 // Usage Syntax: 2834 // 1) map[string]*dynamodb.AttributeValue: where string is the :xyz, and *dynamodb.AttributeValue is { S: aws.String("abc"), }, 2835 // a) map[string]*dynamodb.AttributeValue { ":xyz" : { S: aws.String("abc"), }, ":xyy" : { N: aws.String("123"), }, } 2836 // 2) Add to Map 2837 // a) m := make(map[string]*dynamodb.AttributeValue) 2838 // b) m[":xyz"] = &dynamodb.AttributeValue{ S: aws.String("xyz") } 2839 // 3) Slice of Strings -> CONVERT To Slice of *dynamodb.AttributeValue = []string -> []*dynamodb.AttributeValue 2840 // a) av, err := dynamodbattribute.MarshalList(xyzSlice) 2841 // b) ExpressionAttributeValue, Use 'L' To Represent the List for av defined in 3.a above 2842 // 2843 // filterConditionExpression = optional; ATTRIBUTES ARE CASE SENSITIVE; once query on key conditions returned, this filter condition further restricts return data before output to caller; 2844 // Usage Syntax: 2845 // 1) &expression.Name(xyz).Equals(expression.Value(abc)) 2846 // 2) &expression.Name(xyz).Equals(expression.Value(abc)).And(...) 2847 // 2848 // projectedAttributes = optional; ATTRIBUTES ARE CASE SENSITIVE; variadic list of attribute names that this query will project into result items; 2849 // attribute names must match struct field name or struct tag's json / dynamodbav tag values 2850 // 2851 // Return Values: 2852 // 2853 // prevEvalKey = if paged query, the last evaluate key returned, to be used in subsequent query via exclusiveStartKey; otherwise always nil is returned 2854 // prevEvalkey map is set into exclusiveStartKey field if more data to load 2855 // 2856 // notes: 2857 // 2858 // item struct tags 2859 // use `json:"" dynamodbav:""` 2860 // json = sets the name used in json 2861 // dynamodbav = sets the name used in dynamodb 2862 // reference child element 2863 // if struct has field with complex type (another struct), to reference it in code, use the parent struct field dot child field notation 2864 // Info in parent struct with struct tag as info; to reach child element: info.xyz 2865 func (d *DynamoDB) QueryItems(resultItemsPtr interface{}, 2866 timeOutDuration *time.Duration, 2867 consistentRead *bool, 2868 indexName *string, 2869 pageLimit *int64, 2870 pagedQuery bool, 2871 pagedQueryPageCountLimit *int64, 2872 exclusiveStartKey map[string]*dynamodb.AttributeValue, 2873 keyConditionExpression string, 2874 expressionAttributeNames map[string]*string, 2875 expressionAttributeValues map[string]*dynamodb.AttributeValue, 2876 filterConditionExpression *expression.ConditionBuilder, 2877 projectedAttributes ...string) (prevEvalKey map[string]*dynamodb.AttributeValue, ddbErr *DynamoDBError) { 2878 if xray.XRayServiceOn() { 2879 return d.queryItemsWithTrace(resultItemsPtr, timeOutDuration, consistentRead, indexName, pageLimit, pagedQuery, pagedQueryPageCountLimit, exclusiveStartKey, 2880 keyConditionExpression, expressionAttributeNames, expressionAttributeValues, filterConditionExpression, projectedAttributes...) 2881 } else { 2882 return d.queryItemsNormal(resultItemsPtr, timeOutDuration, consistentRead, indexName, pageLimit, pagedQuery, pagedQueryPageCountLimit, exclusiveStartKey, 2883 keyConditionExpression, expressionAttributeNames, expressionAttributeValues, filterConditionExpression, projectedAttributes...) 2884 } 2885 } 2886 2887 func (d *DynamoDB) queryItemsWithTrace(resultItemsPtr interface{}, 2888 timeOutDuration *time.Duration, 2889 consistentRead *bool, 2890 indexName *string, 2891 pageLimit *int64, 2892 pagedQuery bool, 2893 pagedQueryPageCountLimit *int64, 2894 exclusiveStartKey map[string]*dynamodb.AttributeValue, 2895 keyConditionExpression string, 2896 expressionAttributeNames map[string]*string, 2897 expressionAttributeValues map[string]*dynamodb.AttributeValue, 2898 filterConditionExpression *expression.ConditionBuilder, 2899 projectedAttributes ...string) (prevEvalKey map[string]*dynamodb.AttributeValue, ddbErr *DynamoDBError) { 2900 trace := xray.NewSegment("DynamoDB-QueryItems", d._parentSegment) 2901 defer trace.Close() 2902 defer func() { 2903 if ddbErr != nil { 2904 _ = trace.Seg.AddError(fmt.Errorf(ddbErr.ErrorMessage)) 2905 } 2906 }() 2907 2908 if d.cn == nil { 2909 ddbErr = d.handleError(errors.New("DynamoDB Connection is Required")) 2910 return nil, ddbErr 2911 } 2912 2913 if util.LenTrim(d.TableName) <= 0 { 2914 ddbErr = d.handleError(errors.New("DynamoDB Table Name is Required")) 2915 return nil, ddbErr 2916 } 2917 2918 // result items pointer must be set 2919 if resultItemsPtr == nil { 2920 ddbErr = d.handleError(errors.New("DynamoDB QueryItems Failed: " + "ResultItems is Nil")) 2921 return nil, ddbErr 2922 } 2923 2924 // validate additional input parameters 2925 if util.LenTrim(keyConditionExpression) <= 0 { 2926 ddbErr = d.handleError(errors.New("DynamoDB QueryItems Failed: " + "KeyConditionExpress is Required")) 2927 return nil, ddbErr 2928 } 2929 2930 if expressionAttributeValues == nil { 2931 ddbErr = d.handleError(errors.New("DynamoDB QueryItems Failed: " + "ExpressionAttributeValues is Required")) 2932 return nil, ddbErr 2933 } 2934 2935 // execute dynamodb service 2936 var result *dynamodb.QueryOutput 2937 2938 trace.Capture("QueryItems", func() error { 2939 // gather projection attributes 2940 var proj expression.ProjectionBuilder 2941 projSet := false 2942 2943 if len(projectedAttributes) > 0 { 2944 // compose projected attributes if specified 2945 firstProjectedAttribute := expression.Name(projectedAttributes[0]) 2946 moreProjectedAttributes := []expression.NameBuilder{} 2947 2948 if len(projectedAttributes) > 1 { 2949 firstAttribute := true 2950 2951 for _, v := range projectedAttributes { 2952 if !firstAttribute { 2953 moreProjectedAttributes = append(moreProjectedAttributes, expression.Name(v)) 2954 } else { 2955 firstAttribute = false 2956 } 2957 } 2958 } 2959 2960 if len(moreProjectedAttributes) > 0 { 2961 proj = expression.NamesList(firstProjectedAttribute, moreProjectedAttributes...) 2962 } else { 2963 proj = expression.NamesList(firstProjectedAttribute) 2964 } 2965 2966 projSet = true 2967 } 2968 2969 // compose filter expression and projection if applicable 2970 var expr expression.Expression 2971 var err error 2972 2973 filterSet := false 2974 2975 if filterConditionExpression != nil && projSet { 2976 expr, err = expression.NewBuilder().WithFilter(*filterConditionExpression).WithProjection(proj).Build() 2977 filterSet = true 2978 projSet = true 2979 } else if filterConditionExpression != nil { 2980 expr, err = expression.NewBuilder().WithFilter(*filterConditionExpression).Build() 2981 filterSet = true 2982 projSet = false 2983 } else if projSet { 2984 expr, err = expression.NewBuilder().WithProjection(proj).Build() 2985 filterSet = false 2986 projSet = true 2987 } 2988 2989 if err != nil { 2990 ddbErr = d.handleError(err, "DynamoDB QueryItems Failed (Filter/Projection Expression Build)") 2991 return fmt.Errorf(ddbErr.ErrorMessage) 2992 } 2993 2994 // build query input params 2995 params := &dynamodb.QueryInput{ 2996 TableName: aws.String(d.TableName), 2997 KeyConditionExpression: aws.String(keyConditionExpression), 2998 ExpressionAttributeValues: expressionAttributeValues, 2999 } 3000 3001 if expressionAttributeNames != nil { 3002 params.ExpressionAttributeNames = expressionAttributeNames 3003 } 3004 3005 if filterSet { 3006 params.FilterExpression = expr.Filter() 3007 3008 if params.ExpressionAttributeNames == nil { 3009 params.ExpressionAttributeNames = make(map[string]*string) 3010 } 3011 3012 for k, v := range expr.Names() { 3013 params.ExpressionAttributeNames[k] = v 3014 } 3015 3016 for k, v := range expr.Values() { 3017 params.ExpressionAttributeValues[k] = v 3018 } 3019 } 3020 3021 if projSet { 3022 params.ProjectionExpression = expr.Projection() 3023 3024 if params.ExpressionAttributeNames == nil { 3025 params.ExpressionAttributeNames = expr.Names() 3026 } else { 3027 for k1, v1 := range expr.Names() { 3028 params.ExpressionAttributeNames[k1] = v1 3029 } 3030 } 3031 } 3032 3033 if consistentRead != nil { 3034 if *consistentRead { 3035 if len(*indexName) > 0 { 3036 // gsi not valid for consistent read, turn off consistent read 3037 *consistentRead = false 3038 } 3039 } 3040 3041 params.ConsistentRead = consistentRead 3042 } 3043 3044 if indexName != nil && util.LenTrim(*indexName) > 0 { 3045 params.IndexName = indexName 3046 } 3047 3048 if pageLimit != nil { 3049 params.Limit = pageLimit 3050 } 3051 3052 if exclusiveStartKey != nil { 3053 params.ExclusiveStartKey = exclusiveStartKey 3054 } 3055 3056 // record params payload 3057 d.LastExecuteParamsPayload = "QueryItems = " + params.String() 3058 3059 subTrace := trace.NewSubSegment("QueryItems_Do") 3060 defer subTrace.Close() 3061 3062 if timeOutDuration != nil { 3063 ctx, cancel := context.WithTimeout(subTrace.Ctx, *timeOutDuration) 3064 defer cancel() 3065 result, err = d.do_Query(params, pagedQuery, pagedQueryPageCountLimit, ctx) 3066 } else { 3067 result, err = d.do_Query(params, pagedQuery, pagedQueryPageCountLimit, subTrace.Ctx) 3068 } 3069 3070 if err != nil { 3071 ddbErr = d.handleError(err, "DynamoDB QueryItems Failed: (QueryItems)") 3072 return fmt.Errorf(ddbErr.ErrorMessage) 3073 } 3074 3075 if result == nil { 3076 return nil 3077 } 3078 3079 // unmarshal result items to target object map 3080 if err = dynamodbattribute.UnmarshalListOfMaps(result.Items, resultItemsPtr); err != nil { 3081 ddbErr = d.handleError(err, "Dynamo QueryItems Failed: (Unmarshal Result Items)") 3082 return fmt.Errorf(ddbErr.ErrorMessage) 3083 } else { 3084 return nil 3085 } 3086 }, &xray.XTraceData{ 3087 Meta: map[string]interface{}{ 3088 "TableName": d.TableName, 3089 "IndexName": aws.StringValue(indexName), 3090 "ExclusiveStartKey": exclusiveStartKey, 3091 "KeyConditionExpression": keyConditionExpression, 3092 "ExpressionAttributeNames": expressionAttributeNames, 3093 "ExpressionAttributeValues": expressionAttributeValues, 3094 "FilterConditionExpression": filterConditionExpression, 3095 }, 3096 }) 3097 3098 // query items successful 3099 if result != nil { 3100 return result.LastEvaluatedKey, ddbErr 3101 } else { 3102 return nil, ddbErr 3103 } 3104 } 3105 3106 func (d *DynamoDB) queryItemsNormal(resultItemsPtr interface{}, 3107 timeOutDuration *time.Duration, 3108 consistentRead *bool, 3109 indexName *string, 3110 pageLimit *int64, 3111 pagedQuery bool, 3112 pagedQueryPageCountLimit *int64, 3113 exclusiveStartKey map[string]*dynamodb.AttributeValue, 3114 keyConditionExpression string, 3115 expressionAttributeNames map[string]*string, 3116 expressionAttributeValues map[string]*dynamodb.AttributeValue, 3117 filterConditionExpression *expression.ConditionBuilder, 3118 projectedAttributes ...string) (prevEvalKey map[string]*dynamodb.AttributeValue, ddbErr *DynamoDBError) { 3119 if d.cn == nil { 3120 return nil, d.handleError(errors.New("DynamoDB Connection is Required")) 3121 } 3122 3123 if util.LenTrim(d.TableName) <= 0 { 3124 return nil, d.handleError(errors.New("DynamoDB Table Name is Required")) 3125 } 3126 3127 // result items pointer must be set 3128 if resultItemsPtr == nil { 3129 return nil, d.handleError(errors.New("DynamoDB QueryItems Failed: " + "ResultItems is Nil")) 3130 } 3131 3132 // validate additional input parameters 3133 if util.LenTrim(keyConditionExpression) <= 0 { 3134 return nil, d.handleError(errors.New("DynamoDB QueryItems Failed: " + "KeyConditionExpress is Required")) 3135 } 3136 3137 if expressionAttributeValues == nil { 3138 return nil, d.handleError(errors.New("DynamoDB QueryItems Failed: " + "ExpressionAttributeValues is Required")) 3139 } 3140 3141 // execute dynamodb service 3142 var result *dynamodb.QueryOutput 3143 3144 // gather projection attributes 3145 var proj expression.ProjectionBuilder 3146 projSet := false 3147 3148 if len(projectedAttributes) > 0 { 3149 // compose projected attributes if specified 3150 firstProjectedAttribute := expression.Name(projectedAttributes[0]) 3151 moreProjectedAttributes := []expression.NameBuilder{} 3152 3153 if len(projectedAttributes) > 1 { 3154 firstAttribute := true 3155 3156 for _, v := range projectedAttributes { 3157 if !firstAttribute { 3158 moreProjectedAttributes = append(moreProjectedAttributes, expression.Name(v)) 3159 } else { 3160 firstAttribute = false 3161 } 3162 } 3163 } 3164 3165 if len(moreProjectedAttributes) > 0 { 3166 proj = expression.NamesList(firstProjectedAttribute, moreProjectedAttributes...) 3167 } else { 3168 proj = expression.NamesList(firstProjectedAttribute) 3169 } 3170 3171 projSet = true 3172 } 3173 3174 // compose filter expression and projection if applicable 3175 var expr expression.Expression 3176 var err error 3177 3178 filterSet := false 3179 3180 if filterConditionExpression != nil && projSet { 3181 expr, err = expression.NewBuilder().WithFilter(*filterConditionExpression).WithProjection(proj).Build() 3182 filterSet = true 3183 projSet = true 3184 } else if filterConditionExpression != nil { 3185 expr, err = expression.NewBuilder().WithFilter(*filterConditionExpression).Build() 3186 filterSet = true 3187 projSet = false 3188 } else if projSet { 3189 expr, err = expression.NewBuilder().WithProjection(proj).Build() 3190 filterSet = false 3191 projSet = true 3192 } 3193 3194 if err != nil { 3195 return nil, d.handleError(err, "DynamoDB QueryItems Failed (Filter/Projection Expression Build)") 3196 } 3197 3198 // build query input params 3199 params := &dynamodb.QueryInput{ 3200 TableName: aws.String(d.TableName), 3201 KeyConditionExpression: aws.String(keyConditionExpression), 3202 ExpressionAttributeValues: expressionAttributeValues, 3203 } 3204 3205 if expressionAttributeNames != nil { 3206 params.ExpressionAttributeNames = expressionAttributeNames 3207 } 3208 3209 if filterSet { 3210 params.FilterExpression = expr.Filter() 3211 3212 if params.ExpressionAttributeNames == nil { 3213 params.ExpressionAttributeNames = make(map[string]*string) 3214 } 3215 3216 for k, v := range expr.Names() { 3217 params.ExpressionAttributeNames[k] = v 3218 } 3219 3220 for k, v := range expr.Values() { 3221 params.ExpressionAttributeValues[k] = v 3222 } 3223 } 3224 3225 if projSet { 3226 params.ProjectionExpression = expr.Projection() 3227 3228 if params.ExpressionAttributeNames == nil { 3229 params.ExpressionAttributeNames = expr.Names() 3230 } else { 3231 for k1, v1 := range expr.Names() { 3232 params.ExpressionAttributeNames[k1] = v1 3233 } 3234 } 3235 } 3236 3237 if consistentRead != nil { 3238 if *consistentRead { 3239 if len(*indexName) > 0 { 3240 // gsi not valid for consistent read, turn off consistent read 3241 *consistentRead = false 3242 } 3243 } 3244 3245 params.ConsistentRead = consistentRead 3246 } 3247 3248 if indexName != nil && util.LenTrim(*indexName) > 0 { 3249 params.IndexName = indexName 3250 } 3251 3252 if pageLimit != nil { 3253 params.Limit = pageLimit 3254 } 3255 3256 if exclusiveStartKey != nil { 3257 params.ExclusiveStartKey = exclusiveStartKey 3258 } 3259 3260 // record params payload 3261 d.LastExecuteParamsPayload = "QueryItems = " + params.String() 3262 3263 if timeOutDuration != nil { 3264 ctx, cancel := context.WithTimeout(context.Background(), *timeOutDuration) 3265 defer cancel() 3266 result, err = d.do_Query(params, pagedQuery, pagedQueryPageCountLimit, ctx) 3267 } else { 3268 result, err = d.do_Query(params, pagedQuery, pagedQueryPageCountLimit) 3269 } 3270 3271 if err != nil { 3272 return nil, d.handleError(err, "DynamoDB QueryItems Failed: (QueryItems)") 3273 } 3274 3275 if result == nil { 3276 return nil, d.handleError(err, "DynamoDB QueryItems Failed: (QueryItems)") 3277 } 3278 3279 // unmarshal result items to target object map 3280 if err = dynamodbattribute.UnmarshalListOfMaps(result.Items, resultItemsPtr); err != nil { 3281 ddbErr = d.handleError(err, "Dynamo QueryItems Failed: (Unmarshal Result Items)") 3282 } else { 3283 ddbErr = nil 3284 } 3285 3286 // query items successful 3287 return result.LastEvaluatedKey, ddbErr 3288 } 3289 3290 // QueryItemsWithRetry handles dynamodb retries in case action temporarily fails 3291 // 3292 // warning 3293 // 3294 // projectedAttributes = if specified, must include PartitionKey (Hash key) typically "PK" as the first attribute in projected attributes 3295 func (d *DynamoDB) QueryItemsWithRetry(maxRetries uint, 3296 resultItemsPtr interface{}, 3297 timeOutDuration *time.Duration, 3298 consistentRead *bool, 3299 indexName *string, 3300 pageLimit *int64, 3301 pagedQuery bool, 3302 pagedQueryPageCountLimit *int64, 3303 exclusiveStartKey map[string]*dynamodb.AttributeValue, 3304 keyConditionExpression string, 3305 expressionAttributeNames map[string]*string, 3306 expressionAttributeValues map[string]*dynamodb.AttributeValue, 3307 filterConditionExpression *expression.ConditionBuilder, 3308 projectedAttributes ...string) (prevEvalKey map[string]*dynamodb.AttributeValue, ddbErr *DynamoDBError) { 3309 if maxRetries > 10 { 3310 maxRetries = 10 3311 } 3312 3313 timeout := 5 * time.Second 3314 3315 if timeOutDuration != nil { 3316 timeout = *timeOutDuration 3317 } 3318 3319 if timeout < 5*time.Second { 3320 timeout = 5 * time.Second 3321 } else if timeout > 15*time.Second { 3322 timeout = 15 * time.Second 3323 } 3324 3325 if prevEvalKey, ddbErr = d.QueryItems(resultItemsPtr, util.DurationPtr(timeout), consistentRead, indexName, pageLimit, 3326 pagedQuery, pagedQueryPageCountLimit, exclusiveStartKey, keyConditionExpression, 3327 expressionAttributeNames, expressionAttributeValues, filterConditionExpression, projectedAttributes...); ddbErr != nil { 3328 // has error 3329 if maxRetries > 0 { 3330 if ddbErr.AllowRetry { 3331 if ddbErr.RetryNeedsBackOff { 3332 time.Sleep(500 * time.Millisecond) 3333 } else { 3334 time.Sleep(100 * time.Millisecond) 3335 } 3336 3337 log.Println("QueryItemsWithRetry Failed: " + ddbErr.ErrorMessage) 3338 return d.QueryItemsWithRetry(maxRetries-1, 3339 resultItemsPtr, util.DurationPtr(timeout), consistentRead, indexName, pageLimit, 3340 pagedQuery, pagedQueryPageCountLimit, exclusiveStartKey, keyConditionExpression, 3341 expressionAttributeNames, expressionAttributeValues, filterConditionExpression, projectedAttributes...) 3342 } else { 3343 if ddbErr.SuppressError { 3344 log.Println("QueryItemsWithRetry DynamoDB Error Suppressed, Returning Error Nil (MaxRetries = " + util.UintToStr(maxRetries) + ")") 3345 return nil, nil 3346 } else { 3347 return nil, &DynamoDBError{ 3348 ErrorMessage: "QueryItemsWithRetry Failed: " + ddbErr.ErrorMessage, 3349 SuppressError: false, 3350 AllowRetry: false, 3351 RetryNeedsBackOff: false, 3352 } 3353 } 3354 } 3355 } else { 3356 if ddbErr.SuppressError { 3357 log.Println("QueryItemsWithRetry DynamoDB Error Suppressed, Returning Error Nil (MaxRetries = 0)") 3358 return nil, nil 3359 } else { 3360 return nil, &DynamoDBError{ 3361 ErrorMessage: "QueryItemsWithRetry Failed: (MaxRetries = 0) " + ddbErr.ErrorMessage, 3362 SuppressError: false, 3363 AllowRetry: false, 3364 RetryNeedsBackOff: false, 3365 } 3366 } 3367 } 3368 } else { 3369 // no error 3370 return prevEvalKey, nil 3371 } 3372 } 3373 3374 // QueryPagedItemsWithRetry will query dynamodb items in given table using primary key (PK, SK for example), 3375 // or one of Global/Local Secondary Keys (indexName must be defined if using GSI) 3376 // 3377 // To query against non-key attributes, use Scan (bad for performance however) 3378 // QueryItems requires using Key attributes, and limited to TWO key attributes in condition maximum; 3379 // 3380 // important 3381 // 3382 // if dynamodb table is defined as PK and SK together, then to search without GSI/LSI, MUST use PK and SK together or error will trigger 3383 // 3384 // parameters: 3385 // 3386 // pagedSlicePtr = required, identifies the actual slice pointer for use during paged query 3387 // (this parameter is not the output of result, actual result is returned via return variable returnItemsList) 3388 // resultSlicePtr = required, pointer to working items list struct to contain queried result; 3389 // i.e. []Item{} where Item is struct; if projected attributes less than struct fields, unmatched is defaulted; 3390 // (this parameter is not the output of result, actual result is returned via return variable returnItemsList) 3391 // timeOutDuration = optional, timeout duration sent via context to scan method; nil if not using timeout duration 3392 // consistentRead = (always set to false for paged query internally) 3393 // indexName = optional, global secondary index or local secondary index name to help in query operation 3394 // pageLimit = (always set to 100 internally) 3395 // pagedQuery = (always set to true internally) 3396 // pagedQueryPageCountLimit = (always set to 25 internally) 3397 // exclusiveStartKey = (set internally by the paged query loop if any exists) 3398 // keyConditionExpression = required, ATTRIBUTES ARE CASE SENSITIVE; either the primary key (PK SK for example) or global secondary index (SK Data for example) or another secondary index (secondary index must be named) 3399 // Usage Syntax: 3400 // 1) Max 2 Attribute Fields 3401 // 2) First Field must be Partition Key (Must Evaluate to True or False) 3402 // a) = ONLY 3403 // 3) Second Field is Sort Key (May Evaluate to True or False or Range) 3404 // a) =, <, <=, >, >=, BETWEEN, begins_with() 3405 // 4) Combine Two Fields with AND 3406 // 5) When Attribute Name is Reserved Keyword, Use ExpressionAttributeNames to Define #xyz to Alias 3407 // a) Use the #xyz in the KeyConditionExpression such as #yr = :year (:year is Defined ExpressionAttributeValue) 3408 // 6) Example 3409 // a) partitionKeyName = :partitionKeyVal 3410 // b) partitionKeyName = :partitionKeyVal AND sortKeyName = :sortKeyVal 3411 // c) #yr = :year 3412 // 7) If Using GSI / Local Index 3413 // a) When Using, Must Specify the IndexName 3414 // b) First Field is the GSI's Partition Key, such as SK (Evals to True/False), While Second Field is the GSI's SortKey (Range) 3415 // expressionAttributeNames = (always nil internally, not used in paged query) 3416 // expressionAttributeValues = required, ATTRIBUTES ARE CASE SENSITIVE; sets the value token and value actual to be used within the keyConditionExpression; this sets both compare token and compare value 3417 // Usage Syntax: 3418 // 1) map[string]*dynamodb.AttributeValue: where string is the :xyz, and *dynamodb.AttributeValue is { S: aws.String("abc"), }, 3419 // a) map[string]*dynamodb.AttributeValue { ":xyz" : { S: aws.String("abc"), }, ":xyy" : { N: aws.String("123"), }, } 3420 // 2) Add to Map 3421 // a) m := make(map[string]*dynamodb.AttributeValue) 3422 // b) m[":xyz"] = &dynamodb.AttributeValue{ S: aws.String("xyz") } 3423 // 3) Slice of Strings -> CONVERT To Slice of *dynamodb.AttributeValue = []string -> []*dynamodb.AttributeValue 3424 // a) av, err := dynamodbattribute.MarshalList(xyzSlice) 3425 // b) ExpressionAttributeValue, Use 'L' To Represent the List for av defined in 3.a above 3426 // filterConditionExpression = optional; ATTRIBUTES ARE CASE SENSITIVE; once query on key conditions returned, this filter condition further restricts return data before output to caller; 3427 // Usage Syntax: 3428 // 1) &expression.Name(xyz).Equals(expression.Value(abc)) 3429 // 2) &expression.Name(xyz).Equals(expression.Value(abc)).And(...) 3430 // projectedAttributes = (always nil internally for paged query) 3431 // 3432 // Return Values: 3433 // 3434 // returnItemsList = interface{} of return slice, use assert to cast to target type 3435 // err = error info if error is encountered 3436 // 3437 // notes: 3438 // 3439 // item struct tags 3440 // use `json:"" dynamodbav:""` 3441 // json = sets the name used in json 3442 // dynamodbav = sets the name used in dynamodb 3443 // reference child element 3444 // if struct has field with complex type (another struct), to reference it in code, use the parent struct field dot child field notation 3445 // Info in parent struct with struct tag as info; to reach child element: info.xyz 3446 func (d *DynamoDB) QueryPagedItemsWithRetry(maxRetries uint, 3447 pagedSlicePtr interface{}, 3448 resultSlicePtr interface{}, 3449 timeOutDuration *time.Duration, 3450 indexName string, 3451 keyConditionExpression string, 3452 expressionAttributeValues map[string]*dynamodb.AttributeValue, 3453 filterConditionExpression *expression.ConditionBuilder) (returnItemsList interface{}, err error) { 3454 3455 if pagedSlicePtr == nil { 3456 return nil, fmt.Errorf("PagedSlicePtr Identifies Working Slice Pointer During Query and is Required") 3457 } else { 3458 if valPaged := reflect.ValueOf(pagedSlicePtr); valPaged.Kind() != reflect.Ptr { 3459 return nil, fmt.Errorf("PagedSlicePtr Expected To Be Slice Pointer (Not Ptr)") 3460 } else if valPaged.Elem().Kind() != reflect.Slice { 3461 return nil, fmt.Errorf("PagedSlicePtr Expected To Be Slice Pointer (Not Slice)") 3462 } 3463 } 3464 3465 if resultSlicePtr == nil { 3466 return nil, fmt.Errorf("ResultSlicePtr Contains Query Result in Slice Pointer and is Required") 3467 } else { 3468 if valResult := reflect.ValueOf(resultSlicePtr); valResult.Kind() != reflect.Ptr { 3469 return nil, fmt.Errorf("ResultSlicePtr Expected To Be Slice Pointer (Not Ptr)") 3470 } else if valResult.Elem().Kind() != reflect.Slice { 3471 return nil, fmt.Errorf("ResultSlicePtr Expected To Be Slice Pointer (Not Slice)") 3472 } 3473 } 3474 3475 var prevEvalKey map[string]*dynamodb.AttributeValue 3476 prevEvalKey = nil 3477 3478 var e *DynamoDBError 3479 3480 pageLimit := int64(250) // changed from 100 to 250, since typical record is 4k or less and 250 is about 1mb or less 3481 pagedQueryPageCountLimit := int64(1) // changed to 1 from 25 3482 3483 var indexNamePtr *string 3484 3485 if util.LenTrim(indexName) > 0 { 3486 indexNamePtr = aws.String(indexName) 3487 } else { 3488 indexNamePtr = nil 3489 } 3490 3491 var pagedSliceTemp reflect.Value 3492 // Initialize pagedSliceTemp to avoid encountering the 'call of unknown method on zero Value' issue with subsequent reflect.AppendSlice calls. 3493 if reflect.TypeOf(resultSlicePtr).Kind() == reflect.Ptr { 3494 pagedSliceTemp = reflect.ValueOf(resultSlicePtr).Elem() 3495 } else { 3496 pagedSliceTemp = reflect.ValueOf(resultSlicePtr) 3497 } 3498 3499 for { 3500 // We create a new `pagedSlicePtr` variable for each `for` loop iteration instead of reusing the same one 3501 // because it is a pointer. When using `reflect.AppendSlice`, only a pointer struct is copied into the slice. 3502 // Each iteration of the `dynamodbattribute.UnmarshalListOfMaps` method modifies the content pointed to by this pointer, 3503 // resulting in the slice containing data from only the last iteration. 3504 originalSliceValue := reflect.ValueOf(pagedSlicePtr).Elem() 3505 newPagedSlice := reflect.MakeSlice(originalSliceValue.Type(), 0, 0) 3506 newPagedSlicePtr := reflect.New(newPagedSlice.Type()).Interface() 3507 3508 // each time queried, we process up to 25 pages with each page up to 100 items, 3509 // if there are more data, the prevEvalKey will contain value, 3510 // so the for loop will continue query again until prevEvalKey is nil, 3511 // this method will retrieve all filtered data from data store, but may take longer time if there are more data 3512 if prevEvalKey, e = d.QueryItemsWithRetry(maxRetries, newPagedSlicePtr, timeOutDuration, nil, indexNamePtr, 3513 aws.Int64(pageLimit), true, aws.Int64(pagedQueryPageCountLimit), prevEvalKey, 3514 keyConditionExpression, nil, expressionAttributeValues, 3515 filterConditionExpression); e != nil { 3516 // error 3517 return nil, fmt.Errorf("QueryPagedItemsWithRetry Failed: %s", e) 3518 } else { 3519 // success 3520 //var valTarget reflect.Value 3521 3522 //if reflect.TypeOf(resultSlicePtr).Kind() == reflect.Ptr { 3523 // valTarget = reflect.ValueOf(resultSlicePtr).Elem() 3524 //} else { 3525 // valTarget = reflect.ValueOf(resultSlicePtr) 3526 //} 3527 3528 //val := reflect.AppendSlice(valTarget, reflect.ValueOf(pagedSlicePtr).Elem()) 3529 //resultSlicePtr = val.Interface() 3530 pagedSliceTemp = reflect.AppendSlice(pagedSliceTemp, reflect.ValueOf(newPagedSlicePtr).Elem()) 3531 3532 if prevEvalKey == nil { 3533 break 3534 } 3535 3536 if len(prevEvalKey) == 0 { 3537 break 3538 } 3539 } 3540 } 3541 3542 resultSlicePtr = pagedSliceTemp.Interface() 3543 3544 return resultSlicePtr, nil 3545 } 3546 3547 // QueryPerPageItemsWithRetry will query dynamodb items in given table using primary key (PK, SK for example), 3548 // or one of Global/Local Secondary Keys (indexName must be defined if using GSI); 3549 // 3550 // *** This Query is used for pagination where each query returns a specified set of items, along with the prevEvalKey, 3551 // in the subsequent paged queries using this method, passing prevEvalKey to the exclusiveStartKey parameter will return 3552 // next page of items from the exclusiveStartKey position *** 3553 // 3554 // To query against non-key attributes, use Scan (bad for performance however) 3555 // QueryItems requires using Key attributes, and limited to TWO key attributes in condition maximum; 3556 // 3557 // important 3558 // 3559 // if dynamodb table is defined as PK and SK together, then to search without GSI/LSI, MUST use PK and SK together or error will trigger 3560 // 3561 // parameters: 3562 // 3563 // maxRetries = number of retries to attempt 3564 // itemsPerPage = query per page items count, if < 0 = 10; if > 500 = 500; defaults to 10 if 0 3565 // exclusiveStartKey = if query is continuation from prior pagination, then the prior query's prevEvalKey is passed into this field 3566 // pagedSlicePtr = required, identifies the actual slice pointer for use during paged query 3567 // i.e. []Item{} where Item is struct; if projected attributes less than struct fields, unmatched is defaulted; 3568 // (this parameter is not the output of result, actual result is returned via return variable returnItemsList) 3569 // timeOutDuration = optional, timeout duration sent via context to scan method; nil if not using timeout duration 3570 // indexName = optional, global secondary index or local secondary index name to help in query operation 3571 // keyConditionExpression = required, ATTRIBUTES ARE CASE SENSITIVE; either the primary key (PK SK for example) or global secondary index (SK Data for example) or another secondary index (secondary index must be named) 3572 // Usage Syntax: 3573 // 1) Max 2 Attribute Fields 3574 // 2) First Field must be Partition Key (Must Evaluate to True or False) 3575 // a) = ONLY 3576 // 3) Second Field is Sort Key (May Evaluate to True or False or Range) 3577 // a) =, <, <=, >, >=, BETWEEN, begins_with() 3578 // 4) Combine Two Fields with AND 3579 // 5) When Attribute Name is Reserved Keyword, Use ExpressionAttributeNames to Define #xyz to Alias 3580 // a) Use the #xyz in the KeyConditionExpression such as #yr = :year (:year is Defined ExpressionAttributeValue) 3581 // 6) Example 3582 // a) partitionKeyName = :partitionKeyVal 3583 // b) partitionKeyName = :partitionKeyVal AND sortKeyName = :sortKeyVal 3584 // c) #yr = :year 3585 // 7) If Using GSI / Local Index 3586 // a) When Using, Must Specify the IndexName 3587 // b) First Field is the GSI's Partition Key, such as SK (Evals to True/False), While Second Field is the GSI's SortKey (Range) 3588 // expressionAttributeValues = required, ATTRIBUTES ARE CASE SENSITIVE; sets the value token and value actual to be used within the keyConditionExpression; this sets both compare token and compare value 3589 // Usage Syntax: 3590 // 1) map[string]*dynamodb.AttributeValue: where string is the :xyz, and *dynamodb.AttributeValue is { S: aws.String("abc"), }, 3591 // a) map[string]*dynamodb.AttributeValue { ":xyz" : { S: aws.String("abc"), }, ":xyy" : { N: aws.String("123"), }, } 3592 // 2) Add to Map 3593 // a) m := make(map[string]*dynamodb.AttributeValue) 3594 // b) m[":xyz"] = &dynamodb.AttributeValue{ S: aws.String("xyz") } 3595 // 3) Slice of Strings -> CONVERT To Slice of *dynamodb.AttributeValue = []string -> []*dynamodb.AttributeValue 3596 // a) av, err := dynamodbattribute.MarshalList(xyzSlice) 3597 // b) ExpressionAttributeValue, Use 'L' To Represent the List for av defined in 3.a above 3598 // filterConditionExpression = optional; ATTRIBUTES ARE CASE SENSITIVE; once query on key conditions returned, this filter condition further restricts return data before output to caller; 3599 // Usage Syntax: 3600 // 1) &expression.Name(xyz).Equals(expression.Value(abc)) 3601 // 2) &expression.Name(xyz).Equals(expression.Value(abc)).And(...) 3602 // 3603 // Return Values: 3604 // 3605 // returnItemsList = interface{} of return slice, use assert to cast to target type 3606 // prevEvalKey = map[string]*dynamodb.Attribute, if there are more pages, this value is then used in the subsequent query's exclusiveStartKey parameter 3607 // err = error info if error is encountered 3608 // 3609 // notes: 3610 // 3611 // item struct tags 3612 // use `json:"" dynamodbav:""` 3613 // json = sets the name used in json 3614 // dynamodbav = sets the name used in dynamodb 3615 // reference child element 3616 // if struct has field with complex type (another struct), to reference it in code, use the parent struct field dot child field notation 3617 // Info in parent struct with struct tag as info; to reach child element: info.xyz 3618 func (d *DynamoDB) QueryPerPageItemsWithRetry( 3619 maxRetries uint, 3620 itemsPerPage int64, 3621 exclusiveStartKey map[string]*dynamodb.AttributeValue, 3622 pagedSlicePtr interface{}, 3623 timeOutDuration *time.Duration, 3624 indexName string, 3625 keyConditionExpression string, 3626 expressionAttributeValues map[string]*dynamodb.AttributeValue, 3627 filterConditionExpression *expression.ConditionBuilder) (returnItemsList interface{}, prevEvalKey map[string]*dynamodb.AttributeValue, err error) { 3628 3629 if pagedSlicePtr == nil { 3630 return nil, nil, fmt.Errorf("PagedSlicePtr Identifies Working Slice Pointer During Query and is Required") 3631 } else { 3632 if valPaged := reflect.ValueOf(pagedSlicePtr); valPaged.Kind() != reflect.Ptr { 3633 return nil, nil, fmt.Errorf("PagedSlicePtr Expected To Be Slice Pointer (Not Ptr)") 3634 } else if valPaged.Elem().Kind() != reflect.Slice { 3635 return nil, nil, fmt.Errorf("PagedSlicePtr Expected To Be Slice Pointer (Not Slice)") 3636 } 3637 } 3638 3639 var e *DynamoDBError 3640 3641 if itemsPerPage <= 0 { 3642 itemsPerPage = 10 3643 } else if itemsPerPage > 500 { 3644 itemsPerPage = 500 3645 } 3646 3647 var indexNamePtr *string 3648 3649 if util.LenTrim(indexName) > 0 { 3650 indexNamePtr = aws.String(indexName) 3651 } else { 3652 indexNamePtr = nil 3653 } 3654 3655 if prevEvalKey, e = d.QueryItemsWithRetry(maxRetries, pagedSlicePtr, timeOutDuration, nil, indexNamePtr, 3656 aws.Int64(itemsPerPage), true, aws.Int64(1), exclusiveStartKey, 3657 keyConditionExpression, nil, expressionAttributeValues, 3658 filterConditionExpression); e != nil { 3659 // error 3660 return nil, nil, fmt.Errorf("QueryPerPageItemsWithRetry Failed: %s", e) 3661 } else { 3662 // success 3663 if len(prevEvalKey) == 0 { 3664 prevEvalKey = nil 3665 } 3666 3667 return reflect.ValueOf(pagedSlicePtr).Elem().Interface(), prevEvalKey, nil 3668 } 3669 } 3670 3671 // ScanItems will scan dynamodb items in given table, project results, using filter expression 3672 // >>> DO NOT USE SCAN IF POSSIBLE - SCAN IS NOT EFFICIENT ON RCU <<< 3673 // 3674 // warning 3675 // 3676 // projectedAttributes = if specified, must include PartitionKey (Hash key) typically "PK" as the first attribute in projected attributes 3677 // 3678 // parameters: 3679 // 3680 // resultItemsPtr = required, pointer to items list struct to contain queried result; i.e. []Item{} where Item is struct; if projected attributes less than struct fields, unmatched is defaulted 3681 // timeOutDuration = optional, timeout duration sent via context to scan method; nil if not using timeout duration 3682 // consistentRead = optional, scan uses consistent read or eventual consistent read, default is eventual consistent read 3683 // indexName = optional, global secondary index or local secondary index name to help in scan operation 3684 // pageLimit = optional, scan page limit if set, this limits number of items examined per page during scan operation, allowing scan to work better for RCU 3685 // pagedQuery = optional, indicates if query is page based or not; if true, query will be performed via pages, this helps overcome 1 MB limit of each query result 3686 // pagedQueryPageCountLimit = optional, indicates how many pages to query during paged query action 3687 // exclusiveStartKey = optional, if using pagedQuery and starting the query from prior results 3688 // 3689 // filterConditionExpression = required; ATTRIBUTES ARE CASE SENSITIVE; sets the scan filter condition; 3690 // Usage Syntax: 3691 // 1) expFilter := expression.Name(xyz).Equals(expression.Value(abc)) 3692 // 2) expFilter := expression.Name(xyz).Equals(expression.Value(abc)).And(...) 3693 // 3) Assign expFilter into filterConditionExpression 3694 // 3695 // projectedAttributes = optional; ATTRIBUTES ARE CASE SENSITIVE; variadic list of attribute names that this query will project into result items; 3696 // attribute names must match struct field name or struct tag's json / dynamodbav tag values 3697 // 3698 // Return Values: 3699 // 3700 // prevEvalKey = if paged query, the last evaluate key returned, to be used in subsequent query via exclusiveStartKey; otherwise always nil is returned 3701 // 3702 // notes: 3703 // 3704 // item struct tags 3705 // use `json:"" dynamodbav:""` 3706 // json = sets the name used in json 3707 // dynamodbav = sets the name used in dynamodb 3708 // reference child element 3709 // if struct has field with complex type (another struct), to reference it in code, use the parent struct field dot child field notation 3710 // Info in parent struct with struct tag as info; to reach child element: info.xyz 3711 func (d *DynamoDB) ScanItems(resultItemsPtr interface{}, 3712 timeOutDuration *time.Duration, 3713 consistentRead *bool, 3714 indexName *string, 3715 pageLimit *int64, 3716 pagedQuery bool, 3717 pagedQueryPageCountLimit *int64, 3718 exclusiveStartKey map[string]*dynamodb.AttributeValue, 3719 filterConditionExpression expression.ConditionBuilder, projectedAttributes ...string) (prevEvalKey map[string]*dynamodb.AttributeValue, ddbErr *DynamoDBError) { 3720 if xray.XRayServiceOn() { 3721 return d.scanItemsWithTrace(resultItemsPtr, timeOutDuration, consistentRead, indexName, pageLimit, pagedQuery, pagedQueryPageCountLimit, exclusiveStartKey, filterConditionExpression, projectedAttributes...) 3722 } else { 3723 return d.scanItemsNormal(resultItemsPtr, timeOutDuration, consistentRead, indexName, pageLimit, pagedQuery, pagedQueryPageCountLimit, exclusiveStartKey, filterConditionExpression, projectedAttributes...) 3724 } 3725 } 3726 3727 func (d *DynamoDB) scanItemsWithTrace(resultItemsPtr interface{}, 3728 timeOutDuration *time.Duration, 3729 consistentRead *bool, 3730 indexName *string, 3731 pageLimit *int64, 3732 pagedQuery bool, 3733 pagedQueryPageCountLimit *int64, 3734 exclusiveStartKey map[string]*dynamodb.AttributeValue, 3735 filterConditionExpression expression.ConditionBuilder, projectedAttributes ...string) (prevEvalKey map[string]*dynamodb.AttributeValue, ddbErr *DynamoDBError) { 3736 trace := xray.NewSegment("DynamoDB-ScanItems", d._parentSegment) 3737 defer trace.Close() 3738 defer func() { 3739 if ddbErr != nil { 3740 _ = trace.Seg.AddError(fmt.Errorf(ddbErr.ErrorMessage)) 3741 } 3742 }() 3743 3744 if d.cn == nil { 3745 ddbErr = d.handleError(errors.New("DynamoDB Connection is Required")) 3746 return nil, ddbErr 3747 } 3748 3749 if util.LenTrim(d.TableName) <= 0 { 3750 ddbErr = d.handleError(errors.New("DynamoDB Table Name is Required")) 3751 return nil, ddbErr 3752 } 3753 3754 // result items pointer must be set 3755 if resultItemsPtr == nil { 3756 ddbErr = d.handleError(errors.New("DynamoDB ScanItems Failed: " + "ResultItems is Nil")) 3757 return nil, ddbErr 3758 } 3759 3760 // execute dynamodb service 3761 var result *dynamodb.ScanOutput 3762 3763 trace.Capture("ScanItems", func() error { 3764 // create projected attributes 3765 var proj expression.ProjectionBuilder 3766 projSet := false 3767 3768 if len(projectedAttributes) > 0 { 3769 firstProjectedAttribute := expression.Name(projectedAttributes[0]) 3770 moreProjectedAttributes := []expression.NameBuilder{} 3771 3772 if len(projectedAttributes) > 1 { 3773 firstAttribute := true 3774 3775 for _, v := range projectedAttributes { 3776 if !firstAttribute { 3777 moreProjectedAttributes = append(moreProjectedAttributes, expression.Name(v)) 3778 } else { 3779 firstAttribute = false 3780 } 3781 } 3782 } 3783 3784 if len(moreProjectedAttributes) > 0 { 3785 proj = expression.NamesList(firstProjectedAttribute, moreProjectedAttributes...) 3786 } else { 3787 proj = expression.NamesList(firstProjectedAttribute) 3788 } 3789 3790 projSet = true 3791 } 3792 3793 // build expression 3794 var expr expression.Expression 3795 var err error 3796 3797 if projSet { 3798 expr, err = expression.NewBuilder().WithFilter(filterConditionExpression).WithProjection(proj).Build() 3799 } else { 3800 expr, err = expression.NewBuilder().WithFilter(filterConditionExpression).Build() 3801 } 3802 3803 if err != nil { 3804 ddbErr = d.handleError(err, "DynamoDB ScanItems Failed: (Expression NewBuilder)") 3805 return fmt.Errorf(ddbErr.ErrorMessage) 3806 } 3807 3808 // build query input params 3809 params := &dynamodb.ScanInput{ 3810 TableName: aws.String(d.TableName), 3811 FilterExpression: expr.Filter(), 3812 ExpressionAttributeNames: expr.Names(), 3813 ExpressionAttributeValues: expr.Values(), 3814 } 3815 3816 if projSet { 3817 params.ProjectionExpression = expr.Projection() 3818 params.ExpressionAttributeNames = expr.Names() 3819 } 3820 3821 if consistentRead != nil { 3822 if *consistentRead { 3823 if len(*indexName) > 0 { 3824 // gsi not valid for consistent read, turn off consistent read 3825 *consistentRead = false 3826 } 3827 } 3828 3829 params.ConsistentRead = consistentRead 3830 } 3831 3832 if indexName != nil && util.LenTrim(*indexName) > 0 { 3833 params.IndexName = indexName 3834 } 3835 3836 if pageLimit != nil { 3837 params.Limit = pageLimit 3838 } 3839 3840 if exclusiveStartKey != nil { 3841 params.ExclusiveStartKey = exclusiveStartKey 3842 } 3843 3844 // record params payload 3845 d.LastExecuteParamsPayload = "ScanItems = " + params.String() 3846 3847 subTrace := trace.NewSubSegment("ScanItems_Do") 3848 defer subTrace.Close() 3849 3850 // create timeout context 3851 if timeOutDuration != nil { 3852 ctx, cancel := context.WithTimeout(subTrace.Ctx, *timeOutDuration) 3853 defer cancel() 3854 result, err = d.do_Scan(params, pagedQuery, pagedQueryPageCountLimit, ctx) 3855 } else { 3856 result, err = d.do_Scan(params, pagedQuery, pagedQueryPageCountLimit, subTrace.Ctx) 3857 } 3858 3859 if err != nil { 3860 ddbErr = d.handleError(err, "DynamoDB ScanItems Failed: (ScanItems)") 3861 return fmt.Errorf(ddbErr.ErrorMessage) 3862 } 3863 3864 if result == nil { 3865 return nil 3866 } 3867 3868 // unmarshal result items to target object map 3869 if err = dynamodbattribute.UnmarshalListOfMaps(result.Items, resultItemsPtr); err != nil { 3870 ddbErr = d.handleError(err, "DynamoDB ScanItems Failed: (Unmarshal Result Items)") 3871 return fmt.Errorf(ddbErr.ErrorMessage) 3872 } else { 3873 return nil 3874 } 3875 }, &xray.XTraceData{ 3876 Meta: map[string]interface{}{ 3877 "TableName": d.TableName, 3878 "IndexName": aws.StringValue(indexName), 3879 "ExclusiveStartKey": exclusiveStartKey, 3880 "FilterConditionExpression": filterConditionExpression, 3881 }, 3882 }) 3883 3884 // scan items successful 3885 if result != nil { 3886 return result.LastEvaluatedKey, ddbErr 3887 } else { 3888 return nil, ddbErr 3889 } 3890 } 3891 3892 func (d *DynamoDB) scanItemsNormal(resultItemsPtr interface{}, 3893 timeOutDuration *time.Duration, 3894 consistentRead *bool, 3895 indexName *string, 3896 pageLimit *int64, 3897 pagedQuery bool, 3898 pagedQueryPageCountLimit *int64, 3899 exclusiveStartKey map[string]*dynamodb.AttributeValue, 3900 filterConditionExpression expression.ConditionBuilder, projectedAttributes ...string) (prevEvalKey map[string]*dynamodb.AttributeValue, ddbErr *DynamoDBError) { 3901 if d.cn == nil { 3902 return nil, d.handleError(errors.New("DynamoDB Connection is Required")) 3903 } 3904 3905 if util.LenTrim(d.TableName) <= 0 { 3906 return nil, d.handleError(errors.New("DynamoDB Table Name is Required")) 3907 } 3908 3909 // result items pointer must be set 3910 if resultItemsPtr == nil { 3911 return nil, d.handleError(errors.New("DynamoDB ScanItems Failed: " + "ResultItems is Nil")) 3912 } 3913 3914 // execute dynamodb service 3915 var result *dynamodb.ScanOutput 3916 3917 // create projected attributes 3918 var proj expression.ProjectionBuilder 3919 projSet := false 3920 3921 if len(projectedAttributes) > 0 { 3922 firstProjectedAttribute := expression.Name(projectedAttributes[0]) 3923 moreProjectedAttributes := []expression.NameBuilder{} 3924 3925 if len(projectedAttributes) > 1 { 3926 firstAttribute := true 3927 3928 for _, v := range projectedAttributes { 3929 if !firstAttribute { 3930 moreProjectedAttributes = append(moreProjectedAttributes, expression.Name(v)) 3931 } else { 3932 firstAttribute = false 3933 } 3934 } 3935 } 3936 3937 if len(moreProjectedAttributes) > 0 { 3938 proj = expression.NamesList(firstProjectedAttribute, moreProjectedAttributes...) 3939 } else { 3940 proj = expression.NamesList(firstProjectedAttribute) 3941 } 3942 3943 projSet = true 3944 } 3945 3946 // build expression 3947 var expr expression.Expression 3948 var err error 3949 3950 if projSet { 3951 expr, err = expression.NewBuilder().WithFilter(filterConditionExpression).WithProjection(proj).Build() 3952 } else { 3953 expr, err = expression.NewBuilder().WithFilter(filterConditionExpression).Build() 3954 } 3955 3956 if err != nil { 3957 return nil, d.handleError(err, "DynamoDB ScanItems Failed: (Expression NewBuilder)") 3958 } 3959 3960 // build query input params 3961 params := &dynamodb.ScanInput{ 3962 TableName: aws.String(d.TableName), 3963 FilterExpression: expr.Filter(), 3964 ExpressionAttributeNames: expr.Names(), 3965 ExpressionAttributeValues: expr.Values(), 3966 } 3967 3968 if projSet { 3969 params.ProjectionExpression = expr.Projection() 3970 params.ExpressionAttributeNames = expr.Names() 3971 } 3972 3973 if consistentRead != nil { 3974 if *consistentRead { 3975 if len(*indexName) > 0 { 3976 // gsi not valid for consistent read, turn off consistent read 3977 *consistentRead = false 3978 } 3979 } 3980 3981 params.ConsistentRead = consistentRead 3982 } 3983 3984 if indexName != nil && util.LenTrim(*indexName) > 0 { 3985 params.IndexName = indexName 3986 } 3987 3988 if pageLimit != nil { 3989 params.Limit = pageLimit 3990 } 3991 3992 if exclusiveStartKey != nil { 3993 params.ExclusiveStartKey = exclusiveStartKey 3994 } 3995 3996 // record params payload 3997 d.LastExecuteParamsPayload = "ScanItems = " + params.String() 3998 3999 // create timeout context 4000 if timeOutDuration != nil { 4001 ctx, cancel := context.WithTimeout(context.Background(), *timeOutDuration) 4002 defer cancel() 4003 result, err = d.do_Scan(params, pagedQuery, pagedQueryPageCountLimit, ctx) 4004 } else { 4005 result, err = d.do_Scan(params, pagedQuery, pagedQueryPageCountLimit) 4006 } 4007 4008 if err != nil { 4009 return nil, d.handleError(err, "DynamoDB ScanItems Failed: (ScanItems)") 4010 } 4011 4012 if result == nil { 4013 return nil, d.handleError(err, "DynamoDB ScanItems Failed: (ScanItems)") 4014 } 4015 4016 // unmarshal result items to target object map 4017 if err = dynamodbattribute.UnmarshalListOfMaps(result.Items, resultItemsPtr); err != nil { 4018 ddbErr = d.handleError(err, "DynamoDB ScanItems Failed: (Unmarshal Result Items)") 4019 } else { 4020 ddbErr = nil 4021 } 4022 4023 // scan items successful 4024 return result.LastEvaluatedKey, ddbErr 4025 } 4026 4027 // ScanItemsWithRetry handles dynamodb retries in case action temporarily fails 4028 // 4029 // warning 4030 // 4031 // projectedAttributes = if specified, must include PartitionKey (Hash key) typically "PK" as the first attribute in projected attributes 4032 func (d *DynamoDB) ScanItemsWithRetry(maxRetries uint, 4033 resultItemsPtr interface{}, 4034 timeOutDuration *time.Duration, 4035 consistentRead *bool, 4036 indexName *string, 4037 pageLimit *int64, 4038 pagedQuery bool, 4039 pagedQueryPageCountLimit *int64, 4040 exclusiveStartKey map[string]*dynamodb.AttributeValue, 4041 filterConditionExpression expression.ConditionBuilder, projectedAttributes ...string) (prevEvalKey map[string]*dynamodb.AttributeValue, ddbErr *DynamoDBError) { 4042 if maxRetries > 10 { 4043 maxRetries = 10 4044 } 4045 4046 timeout := 10 * time.Second 4047 4048 if timeOutDuration != nil { 4049 timeout = *timeOutDuration 4050 } 4051 4052 if timeout < 10*time.Second { 4053 timeout = 10 * time.Second 4054 } else if timeout > 30*time.Second { 4055 timeout = 30 * time.Second 4056 } 4057 4058 if prevEvalKey, ddbErr = d.ScanItems(resultItemsPtr, util.DurationPtr(timeout), consistentRead, indexName, pageLimit, 4059 pagedQuery, pagedQueryPageCountLimit, 4060 exclusiveStartKey, filterConditionExpression, projectedAttributes...); ddbErr != nil { 4061 // has error 4062 if maxRetries > 0 { 4063 if ddbErr.AllowRetry { 4064 if ddbErr.RetryNeedsBackOff { 4065 time.Sleep(500 * time.Millisecond) 4066 } else { 4067 time.Sleep(100 * time.Millisecond) 4068 } 4069 4070 log.Println("ScanItemsWithRetry Failed: " + ddbErr.ErrorMessage) 4071 return d.ScanItemsWithRetry(maxRetries-1, 4072 resultItemsPtr, util.DurationPtr(timeout), consistentRead, indexName, pageLimit, 4073 pagedQuery, pagedQueryPageCountLimit, 4074 exclusiveStartKey, filterConditionExpression, projectedAttributes...) 4075 } else { 4076 if ddbErr.SuppressError { 4077 log.Println("ScanItemsWithRetry DynamoDB Error Suppressed, Returning Error Nil (MaxRetries = " + util.UintToStr(maxRetries) + ")") 4078 return nil, nil 4079 } else { 4080 return nil, &DynamoDBError{ 4081 ErrorMessage: "ScanItemsWithRetry Failed: " + ddbErr.ErrorMessage, 4082 SuppressError: false, 4083 AllowRetry: false, 4084 RetryNeedsBackOff: false, 4085 } 4086 } 4087 } 4088 } else { 4089 if ddbErr.SuppressError { 4090 log.Println("ScanItemsWithRetry DynamoDB Error Suppressed, Returning Error Nil (MaxRetries = 0)") 4091 return nil, nil 4092 } else { 4093 return nil, &DynamoDBError{ 4094 ErrorMessage: "ScanItemsWithRetry Failed: (MaxRetries = 0) " + ddbErr.ErrorMessage, 4095 SuppressError: false, 4096 AllowRetry: false, 4097 RetryNeedsBackOff: false, 4098 } 4099 } 4100 } 4101 } else { 4102 // no error 4103 return prevEvalKey, nil 4104 } 4105 } 4106 4107 // ScanPagedItemsWithRetry will scan dynamodb items in given table using paged mode with retry, project results, using filter expression 4108 // >>> DO NOT USE SCAN IF POSSIBLE - SCAN IS NOT EFFICIENT ON RCU <<< 4109 // 4110 // parameters: 4111 // 4112 // maxRetries = required, max number of auto retries per paged query 4113 // pagedSlicePtr = required, working variable to store paged query (actual return items list is via return variable) 4114 // resultSlicePtr = required, pointer to items list struct to contain queried result; i.e. []Item{} where Item is struct; if projected attributes less than struct fields, unmatched is defaulted 4115 // timeOutDuration = optional, timeout duration sent via context to scan method; nil if not using timeout duration 4116 // consistentRead = (always false) 4117 // indexName = optional, global secondary index or local secondary index name to help in scan operation 4118 // pageLimit = (always 100) 4119 // pagedQuery = (always true) 4120 // pagedQueryPageCountLimit = (always 25) 4121 // exclusiveStartKey = (always internally controlled during paged query) 4122 // 4123 // filterConditionExpression = required; ATTRIBUTES ARE CASE SENSITIVE; sets the scan filter condition; 4124 // Usage Syntax: 4125 // 1) expFilter := expression.Name(xyz).Equals(expression.Value(abc)) 4126 // 2) expFilter := expression.Name(xyz).Equals(expression.Value(abc)).And(...) 4127 // 3) Assign expFilter into filterConditionExpression 4128 // 4129 // projectedAttributes = (always project all attributes) 4130 // 4131 // Return Values: 4132 // 4133 // returnItemsList = interface of slice returned, representing the items found during scan 4134 // err = error if encountered 4135 // 4136 // notes: 4137 // 4138 // item struct tags 4139 // use `json:"" dynamodbav:""` 4140 // json = sets the name used in json 4141 // dynamodbav = sets the name used in dynamodb 4142 // reference child element 4143 // if struct has field with complex type (another struct), to reference it in code, use the parent struct field dot child field notation 4144 // Info in parent struct with struct tag as info; to reach child element: info.xyz 4145 func (d *DynamoDB) ScanPagedItemsWithRetry(maxRetries uint, 4146 pagedSlicePtr interface{}, 4147 resultSlicePtr interface{}, 4148 timeOutDuration *time.Duration, 4149 indexName string, 4150 filterConditionExpression expression.ConditionBuilder) (returnItemsList interface{}, err error) { 4151 4152 if pagedSlicePtr == nil { 4153 return nil, fmt.Errorf("PagedSlicePtr Identifies Working Slice Pointer During Scan and is Required") 4154 } else { 4155 if valPaged := reflect.ValueOf(pagedSlicePtr); valPaged.Kind() != reflect.Ptr { 4156 return nil, fmt.Errorf("PagedSlicePtr Expected To Be Slice Pointer (Not Ptr)") 4157 } else if valPaged.Elem().Kind() != reflect.Slice { 4158 return nil, fmt.Errorf("PagedSlicePtr Expected To Be Slice Pointer (Not Slice)") 4159 } 4160 } 4161 4162 if resultSlicePtr == nil { 4163 return nil, fmt.Errorf("ResultSlicePtr Contains Scan Result in Slice Pointer and is Required") 4164 } else { 4165 if valResult := reflect.ValueOf(resultSlicePtr); valResult.Kind() != reflect.Ptr { 4166 return nil, fmt.Errorf("ResultSlicePtr Expected To Be Slice Pointer (Not Ptr)") 4167 } else if valResult.Elem().Kind() != reflect.Slice { 4168 return nil, fmt.Errorf("ResultSlicePtr Expected To Be Slice Pointer (Not Slice)") 4169 } 4170 } 4171 4172 var prevEvalKey map[string]*dynamodb.AttributeValue 4173 prevEvalKey = nil 4174 4175 var e *DynamoDBError 4176 4177 pageLimit := int64(100) 4178 pagedQueryPageCountLimit := int64(25) 4179 4180 var indexNamePtr *string 4181 4182 if util.LenTrim(indexName) > 0 { 4183 indexNamePtr = aws.String(indexName) 4184 } else { 4185 indexNamePtr = nil 4186 } 4187 4188 for { 4189 if prevEvalKey, e = d.ScanItemsWithRetry(maxRetries, pagedSlicePtr, timeOutDuration, nil, indexNamePtr, 4190 aws.Int64(pageLimit), true, aws.Int64(pagedQueryPageCountLimit), prevEvalKey, filterConditionExpression); e != nil { 4191 // error 4192 return nil, fmt.Errorf("ScanPagedItemsWithRetry Failed: %s", e) 4193 } else { 4194 // success 4195 var valTarget reflect.Value 4196 4197 if reflect.TypeOf(resultSlicePtr).Kind() == reflect.Ptr { 4198 valTarget = reflect.ValueOf(resultSlicePtr).Elem() 4199 } else { 4200 valTarget = reflect.ValueOf(resultSlicePtr) 4201 } 4202 4203 val := reflect.AppendSlice(valTarget, reflect.ValueOf(pagedSlicePtr).Elem()) 4204 resultSlicePtr = val.Interface() 4205 4206 if prevEvalKey == nil { 4207 break 4208 } 4209 4210 if len(prevEvalKey) == 0 { 4211 break 4212 } 4213 } 4214 } 4215 4216 return resultSlicePtr, nil 4217 } 4218 4219 // BatchWriteItems will group up to 25 put and delete items in a single batch, and perform actions in parallel against dynamodb for better write efficiency, 4220 // To update items, use UpdateItem instead for each item needing to be updated instead, BatchWriteItems does not support update items 4221 // 4222 // important 4223 // 4224 // if dynamodb table is defined as PK and SK together, then to search, MUST use PK and SK together or error will trigger 4225 // 4226 // parameters: 4227 // 4228 // putItems = slice of item struct objects to add to table (combine of putItems and deleteItems cannot exceed 25) 4229 // 1) Each element of slice is an struct object to be added, struct object must have PK, SK or another named primary key for example, and other attributes as needed 4230 // 2) putItems interface{} = Expects SLICE of STRUCT OBJECTS 4231 // 4232 // deleteKeys = slice of search keys (as defined by DynamoDBTableKeys struct) to remove from table (combine of putItems and deleteKeys cannot exceed 25) 4233 // 1) Each element of slice is an struct object of DynamoDBTableKeys 4234 // 4235 // timeOutDuration = optional, timeout duration sent via context to scan method; nil if not using timeout duration 4236 // 4237 // return values: 4238 // 4239 // successCount = total number of item actions succeeded 4240 // unprocessedItems = any item actions did not succeed is returned; nil means all processed 4241 // err = if method call failed, error is returned 4242 // 4243 // notes: 4244 // 4245 // item struct tags 4246 // use `json:"" dynamodbav:""` 4247 // json = sets the name used in json 4248 // dynamodbav = sets the name used in dynamodb 4249 // reference child element 4250 // if struct has field with complex type (another struct), to reference it in code, use the parent struct field dot child field notation 4251 // Info in parent struct with struct tag as info; to reach child element: info.xyz 4252 func (d *DynamoDB) BatchWriteItems(putItems interface{}, 4253 deleteKeys []DynamoDBTableKeys, 4254 timeOutDuration *time.Duration) (successCount int, unprocessedItems *DynamoDBUnprocessedItemsAndKeys, err *DynamoDBError) { 4255 if xray.XRayServiceOn() { 4256 return d.batchWriteItemsWithTrace(putItems, deleteKeys, timeOutDuration) 4257 } else { 4258 return d.batchWriteItemsNormal(putItems, deleteKeys, timeOutDuration) 4259 } 4260 } 4261 4262 func (d *DynamoDB) batchWriteItemsWithTrace(putItems interface{}, 4263 deleteKeys []DynamoDBTableKeys, 4264 timeOutDuration *time.Duration) (successCount int, unprocessedItems *DynamoDBUnprocessedItemsAndKeys, err *DynamoDBError) { 4265 trace := xray.NewSegment("DynamoDB-BatchWriteItems", d._parentSegment) 4266 defer trace.Close() 4267 defer func() { 4268 if err != nil { 4269 _ = trace.Seg.AddError(fmt.Errorf(err.ErrorMessage)) 4270 } 4271 }() 4272 4273 if d.cn == nil { 4274 err = d.handleError(errors.New("DynamoDB Connection is Required")) 4275 return 0, nil, err 4276 } 4277 4278 if util.LenTrim(d.TableName) <= 0 { 4279 err = d.handleError(errors.New("DynamoDB Table Name is Required")) 4280 return 0, nil, err 4281 } 4282 4283 // validate input parameters 4284 if putItems == nil && deleteKeys == nil { 4285 err = d.handleError(errors.New("DynamoDB BatchWriteItems Failed: " + "PutItems and DeleteKeys Both Cannot Be Nil")) 4286 return 0, nil, err 4287 } 4288 4289 trace.Capture("BatchWriteItems", func() error { 4290 // marshal put and delete objects 4291 var putItemsAv []map[string]*dynamodb.AttributeValue 4292 var deleteKeysAv []map[string]*dynamodb.AttributeValue 4293 4294 if putItems != nil { 4295 // putItems is in interface 4296 // need to reflect into slice of interface{} 4297 putItemsIf := util.SliceObjectsToSliceInterface(putItems) 4298 4299 if putItemsIf != nil && len(putItemsIf) > 0 { 4300 for _, v := range putItemsIf { 4301 if m, e := dynamodbattribute.MarshalMap(v); e != nil { 4302 successCount = 0 4303 unprocessedItems = nil 4304 err = d.handleError(e, "DynamoDB BatchWriteItems Failed: (PutItems MarshalMap)") 4305 return fmt.Errorf(err.ErrorMessage) 4306 } else { 4307 if m != nil { 4308 putItemsAv = append(putItemsAv, m) 4309 } else { 4310 successCount = 0 4311 unprocessedItems = nil 4312 err = d.handleError(errors.New("DynamoDB BatchWriteItems Failed: (PutItems MarshalMap) " + "PutItem Marshal Result Object Nil")) 4313 return fmt.Errorf(err.ErrorMessage) 4314 } 4315 } 4316 } 4317 } 4318 } 4319 4320 if deleteKeys != nil { 4321 if len(deleteKeys) > 0 { 4322 for _, v := range deleteKeys { 4323 if m, e := dynamodbattribute.MarshalMap(v); e != nil { 4324 successCount = 0 4325 unprocessedItems = nil 4326 err = d.handleError(e, "DynamoDB BatchWriteItems Failed: (DeleteKeys MarshalMap)") 4327 return fmt.Errorf(err.ErrorMessage) 4328 } else { 4329 if m != nil { 4330 deleteKeysAv = append(deleteKeysAv, m) 4331 } else { 4332 successCount = 0 4333 unprocessedItems = nil 4334 err = d.handleError(errors.New("DynamoDB BatchWriteItems Failed: (DeleteKeys MarshalMap) " + "DeleteKey Marshal Result Object Nil")) 4335 return fmt.Errorf(err.ErrorMessage) 4336 } 4337 } 4338 } 4339 } 4340 } 4341 4342 putCount := 0 4343 deleteCount := 0 4344 4345 if putItemsAv != nil { 4346 putCount = len(putItemsAv) 4347 } 4348 4349 if deleteKeysAv != nil { 4350 deleteCount = len(deleteKeysAv) 4351 } 4352 4353 if (putCount+deleteCount) <= 0 || (putCount+deleteCount) > 25 { 4354 successCount = 0 4355 unprocessedItems = nil 4356 err = d.handleError(errors.New("DynamoDB BatchWriteItems Failed: " + "PutItems and DeleteKeys Count Must Be 1 to 25 Only")) 4357 return fmt.Errorf(err.ErrorMessage) 4358 } 4359 4360 // holder of delete and put item write requests 4361 var writeRequests []*dynamodb.WriteRequest 4362 4363 // define requestItems wrapper 4364 if deleteCount > 0 { 4365 for _, v := range deleteKeysAv { 4366 writeRequests = append(writeRequests, &dynamodb.WriteRequest{ 4367 DeleteRequest: &dynamodb.DeleteRequest{ 4368 Key: v, 4369 }, 4370 }) 4371 } 4372 } 4373 4374 if putCount > 0 { 4375 for _, v := range putItemsAv { 4376 writeRequests = append(writeRequests, &dynamodb.WriteRequest{ 4377 PutRequest: &dynamodb.PutRequest{ 4378 Item: v, 4379 }, 4380 }) 4381 } 4382 } 4383 4384 // compose batch write params 4385 params := &dynamodb.BatchWriteItemInput{ 4386 RequestItems: map[string][]*dynamodb.WriteRequest{ 4387 d.TableName: writeRequests, 4388 }, 4389 } 4390 4391 // record params payload 4392 d.LastExecuteParamsPayload = "BatchWriteItems = " + params.String() 4393 4394 // execute batch write action 4395 var result *dynamodb.BatchWriteItemOutput 4396 var err1 error 4397 4398 subTrace := trace.NewSubSegment("BatchWriteItems_Do") 4399 defer subTrace.Close() 4400 4401 if timeOutDuration != nil { 4402 ctx, cancel := context.WithTimeout(subTrace.Ctx, *timeOutDuration) 4403 defer cancel() 4404 result, err1 = d.do_BatchWriteItem(params, ctx) 4405 } else { 4406 result, err1 = d.do_BatchWriteItem(params, subTrace.Ctx) 4407 } 4408 4409 if err1 != nil { 4410 successCount = 0 4411 unprocessedItems = nil 4412 err = d.handleError(err1, "DynamoDB BatchWriteItems Failed: (BatchWriteItem)") 4413 return fmt.Errorf(err.ErrorMessage) 4414 } 4415 4416 // evaluate results 4417 unprocessed := result.UnprocessedItems 4418 4419 if unprocessed != nil { 4420 list := unprocessed[d.TableName] 4421 4422 if list != nil && len(list) > 0 { 4423 outList := new(DynamoDBUnprocessedItemsAndKeys) 4424 4425 for _, v := range list { 4426 if v.PutRequest != nil && v.PutRequest.Item != nil { 4427 outList.PutItems = append(outList.PutItems, v.PutRequest.Item) 4428 } 4429 4430 if v.DeleteRequest != nil && v.DeleteRequest.Key != nil { 4431 var o DynamoDBTableKeys 4432 4433 if e := dynamodbattribute.UnmarshalMap(v.DeleteRequest.Key, &o); e == nil { 4434 outList.DeleteKeys = append(outList.DeleteKeys, &o) 4435 } 4436 } 4437 } 4438 4439 successCount = deleteCount + putCount - len(list) 4440 unprocessedItems = outList 4441 err = nil 4442 return nil 4443 } 4444 } 4445 4446 successCount = deleteCount + putCount 4447 unprocessedItems = nil 4448 err = nil 4449 return nil 4450 }, &xray.XTraceData{ 4451 Meta: map[string]interface{}{ 4452 "TableName": d.TableName, 4453 "PutItems": putItems, 4454 "DeleteKeys": deleteKeys, 4455 }, 4456 }) 4457 4458 // batch put and delete items successful 4459 return successCount, unprocessedItems, err 4460 } 4461 4462 func (d *DynamoDB) batchWriteItemsNormal(putItems interface{}, 4463 deleteKeys []DynamoDBTableKeys, 4464 timeOutDuration *time.Duration) (successCount int, unprocessedItems *DynamoDBUnprocessedItemsAndKeys, err *DynamoDBError) { 4465 if d.cn == nil { 4466 return 0, nil, d.handleError(errors.New("DynamoDB Connection is Required")) 4467 } 4468 4469 if util.LenTrim(d.TableName) <= 0 { 4470 return 0, nil, d.handleError(errors.New("DynamoDB Table Name is Required")) 4471 } 4472 4473 // validate input parameters 4474 if putItems == nil && deleteKeys == nil { 4475 return 0, nil, d.handleError(errors.New("DynamoDB BatchWriteItems Failed: " + "PutItems and DeleteKeys Both Cannot Be Nil")) 4476 } 4477 4478 // marshal put and delete objects 4479 var putItemsAv []map[string]*dynamodb.AttributeValue 4480 var deleteKeysAv []map[string]*dynamodb.AttributeValue 4481 4482 if putItems != nil { 4483 // putItems is in interface 4484 // need to reflect into slice of interface{} 4485 putItemsIf := util.SliceObjectsToSliceInterface(putItems) 4486 4487 if putItemsIf != nil && len(putItemsIf) > 0 { 4488 for _, v := range putItemsIf { 4489 if m, e := dynamodbattribute.MarshalMap(v); e != nil { 4490 successCount = 0 4491 unprocessedItems = nil 4492 err = d.handleError(e, "DynamoDB BatchWriteItems Failed: (PutItems MarshalMap)") 4493 return successCount, unprocessedItems, err 4494 } else { 4495 if m != nil { 4496 putItemsAv = append(putItemsAv, m) 4497 } else { 4498 successCount = 0 4499 unprocessedItems = nil 4500 err = d.handleError(errors.New("DynamoDB BatchWriteItems Failed: (PutItems MarshalMap) " + "PutItem Marshal Result Object Nil")) 4501 return successCount, unprocessedItems, err 4502 } 4503 } 4504 } 4505 } 4506 } 4507 4508 if deleteKeys != nil { 4509 if len(deleteKeys) > 0 { 4510 for _, v := range deleteKeys { 4511 if m, e := dynamodbattribute.MarshalMap(v); e != nil { 4512 successCount = 0 4513 unprocessedItems = nil 4514 err = d.handleError(e, "DynamoDB BatchWriteItems Failed: (DeleteKeys MarshalMap)") 4515 return successCount, unprocessedItems, err 4516 } else { 4517 if m != nil { 4518 deleteKeysAv = append(deleteKeysAv, m) 4519 } else { 4520 successCount = 0 4521 unprocessedItems = nil 4522 err = d.handleError(errors.New("DynamoDB BatchWriteItems Failed: (DeleteKeys MarshalMap) " + "DeleteKey Marshal Result Object Nil")) 4523 return successCount, unprocessedItems, err 4524 } 4525 } 4526 } 4527 } 4528 } 4529 4530 putCount := 0 4531 deleteCount := 0 4532 4533 if putItemsAv != nil { 4534 putCount = len(putItemsAv) 4535 } 4536 4537 if deleteKeysAv != nil { 4538 deleteCount = len(deleteKeysAv) 4539 } 4540 4541 if (putCount+deleteCount) <= 0 || (putCount+deleteCount) > 25 { 4542 successCount = 0 4543 unprocessedItems = nil 4544 err = d.handleError(errors.New("DynamoDB BatchWriteItems Failed: " + "PutItems and DeleteKeys Count Must Be 1 to 25 Only")) 4545 return successCount, unprocessedItems, err 4546 } 4547 4548 // holder of delete and put item write requests 4549 var writeRequests []*dynamodb.WriteRequest 4550 4551 // define requestItems wrapper 4552 if deleteCount > 0 { 4553 for _, v := range deleteKeysAv { 4554 writeRequests = append(writeRequests, &dynamodb.WriteRequest{ 4555 DeleteRequest: &dynamodb.DeleteRequest{ 4556 Key: v, 4557 }, 4558 }) 4559 } 4560 } 4561 4562 if putCount > 0 { 4563 for _, v := range putItemsAv { 4564 writeRequests = append(writeRequests, &dynamodb.WriteRequest{ 4565 PutRequest: &dynamodb.PutRequest{ 4566 Item: v, 4567 }, 4568 }) 4569 } 4570 } 4571 4572 // compose batch write params 4573 params := &dynamodb.BatchWriteItemInput{ 4574 RequestItems: map[string][]*dynamodb.WriteRequest{ 4575 d.TableName: writeRequests, 4576 }, 4577 } 4578 4579 // record params payload 4580 d.LastExecuteParamsPayload = "BatchWriteItems = " + params.String() 4581 4582 // execute batch write action 4583 var result *dynamodb.BatchWriteItemOutput 4584 var err1 error 4585 4586 if timeOutDuration != nil { 4587 ctx, cancel := context.WithTimeout(context.Background(), *timeOutDuration) 4588 defer cancel() 4589 result, err1 = d.do_BatchWriteItem(params, ctx) 4590 } else { 4591 result, err1 = d.do_BatchWriteItem(params) 4592 } 4593 4594 if err1 != nil { 4595 successCount = 0 4596 unprocessedItems = nil 4597 err = d.handleError(err1, "DynamoDB BatchWriteItems Failed: (BatchWriteItem)") 4598 return successCount, unprocessedItems, err 4599 } 4600 4601 // evaluate results 4602 unprocessed := result.UnprocessedItems 4603 4604 if unprocessed != nil { 4605 list := unprocessed[d.TableName] 4606 4607 if list != nil && len(list) > 0 { 4608 outList := new(DynamoDBUnprocessedItemsAndKeys) 4609 4610 for _, v := range list { 4611 if v.PutRequest != nil && v.PutRequest.Item != nil { 4612 outList.PutItems = append(outList.PutItems, v.PutRequest.Item) 4613 } 4614 4615 if v.DeleteRequest != nil && v.DeleteRequest.Key != nil { 4616 var o DynamoDBTableKeys 4617 4618 if e := dynamodbattribute.UnmarshalMap(v.DeleteRequest.Key, &o); e == nil { 4619 outList.DeleteKeys = append(outList.DeleteKeys, &o) 4620 } 4621 } 4622 } 4623 4624 successCount = deleteCount + putCount - len(list) 4625 unprocessedItems = outList 4626 err = nil 4627 return successCount, unprocessedItems, err 4628 } 4629 } 4630 4631 successCount = deleteCount + putCount 4632 unprocessedItems = nil 4633 err = nil 4634 4635 // batch put and delete items successful 4636 return successCount, unprocessedItems, err 4637 } 4638 4639 // BatchWriteItemsWithRetry handles dynamodb retries in case action temporarily fails 4640 func (d *DynamoDB) BatchWriteItemsWithRetry(maxRetries uint, 4641 putItems interface{}, deleteKeys []DynamoDBTableKeys, 4642 timeOutDuration *time.Duration) (successCount int, unprocessedItems *DynamoDBUnprocessedItemsAndKeys, err *DynamoDBError) { 4643 if maxRetries > 10 { 4644 maxRetries = 10 4645 } 4646 4647 timeout := 10 * time.Second 4648 4649 if timeOutDuration != nil { 4650 timeout = *timeOutDuration 4651 } 4652 4653 if timeout < 10*time.Second { 4654 timeout = 10 * time.Second 4655 } else if timeout > 30*time.Second { 4656 timeout = 30 * time.Second 4657 } 4658 4659 if successCount, unprocessedItems, err = d.BatchWriteItems(putItems, deleteKeys, util.DurationPtr(timeout)); err != nil { 4660 // has error 4661 if maxRetries > 0 { 4662 if err.AllowRetry { 4663 if err.RetryNeedsBackOff { 4664 time.Sleep(500 * time.Millisecond) 4665 } else { 4666 time.Sleep(100 * time.Millisecond) 4667 } 4668 4669 log.Println("BatchWriteItemsWithRetry Failed: " + err.ErrorMessage) 4670 return d.BatchWriteItemsWithRetry(maxRetries-1, putItems, deleteKeys, util.DurationPtr(timeout)) 4671 } else { 4672 if err.SuppressError { 4673 log.Println("BatchWriteItemsWithRetry DynamoDB Error Suppressed, Returning Error Nil (MaxRetries = " + util.UintToStr(maxRetries) + ")") 4674 return 0, nil, nil 4675 } else { 4676 return 0, nil, &DynamoDBError{ 4677 ErrorMessage: "BatchWriteItemsWithRetry Failed: " + err.ErrorMessage, 4678 SuppressError: false, 4679 AllowRetry: false, 4680 RetryNeedsBackOff: false, 4681 } 4682 } 4683 } 4684 } else { 4685 if err.SuppressError { 4686 log.Println("BatchWriteItemsWithRetry DynamoDB Error Suppressed, Returning Error Nil (MaxRetries = 0)") 4687 return 0, nil, nil 4688 } else { 4689 return 0, nil, &DynamoDBError{ 4690 ErrorMessage: "BatchWriteItemsWithRetry Failed: (MaxRetries = 0) " + err.ErrorMessage, 4691 SuppressError: false, 4692 AllowRetry: false, 4693 RetryNeedsBackOff: false, 4694 } 4695 } 4696 } 4697 } else { 4698 // no error 4699 return successCount, unprocessedItems, nil 4700 } 4701 } 4702 4703 // BatchGetItems accepts a slice of search keys (of DynamoDBSearchKeys struct object), optionally define attribute projections, and return found result items; 4704 // 4705 // important 4706 // 4707 // if dynamodb table is defined as PK and SK together, then to search, MUST use PK and SK together or error will trigger 4708 // 4709 // warning 4710 // 4711 // projectedAttributes = if specified, must include PartitionKey (Hash key) typically "PK" as the first attribute in projected attributes 4712 // 4713 // parameters: 4714 // 4715 // resultItemsPtr = required, pointer to items list struct to contain queried result; i.e. []Item{} where Item is struct; if projected attributes less than struct fields, unmatched is defaulted 4716 // searchKeys = required, slice of DynamoDBTableKeys struct objects to perform search against 4717 // timeOutDuration = optional, timeout duration sent via context to scan method; nil if not using timeout duration 4718 // consistentRead = optional, indicates if the read operation requires consistent read status 4719 // projectedAttributes = optional; ATTRIBUTES ARE CASE SENSITIVE; variadic list of attribute names that this query will project into result items; 4720 // attribute names must match struct field name or struct tag's json / dynamodbav tag values 4721 // 4722 // return values: 4723 // 4724 // notFound = true if no items found; if error encountered, this field returns false with error field filled 4725 // err = if error is encountered, this field will be filled; otherwise nil 4726 // 4727 // notes: 4728 // 4729 // item struct tags 4730 // use `json:"" dynamodbav:""` 4731 // json = sets the name used in json 4732 // dynamodbav = sets the name used in dynamodb 4733 // reference child element 4734 // if struct has field with complex type (another struct), to reference it in code, use the parent struct field dot child field notation 4735 // Info in parent struct with struct tag as info; to reach child element: info.xyz 4736 func (d *DynamoDB) BatchGetItems(resultItemsPtr interface{}, 4737 searchKeys []DynamoDBTableKeys, 4738 timeOutDuration *time.Duration, 4739 consistentRead *bool, 4740 projectedAttributes ...string) (notFound bool, err *DynamoDBError) { 4741 if xray.XRayServiceOn() { 4742 return d.batchGetItemsWithTrace(resultItemsPtr, searchKeys, timeOutDuration, consistentRead, projectedAttributes...) 4743 } else { 4744 return d.batchGetItemsNormal(resultItemsPtr, searchKeys, timeOutDuration, consistentRead, projectedAttributes...) 4745 } 4746 } 4747 4748 func (d *DynamoDB) batchGetItemsWithTrace(resultItemsPtr interface{}, 4749 searchKeys []DynamoDBTableKeys, 4750 timeOutDuration *time.Duration, 4751 consistentRead *bool, 4752 projectedAttributes ...string) (notFound bool, err *DynamoDBError) { 4753 trace := xray.NewSegment("DynamoDB-BatchGetItems", d._parentSegment) 4754 defer trace.Close() 4755 defer func() { 4756 if err != nil { 4757 _ = trace.Seg.AddError(fmt.Errorf(err.ErrorMessage)) 4758 } 4759 }() 4760 4761 if d.cn == nil { 4762 err = d.handleError(errors.New("DynamoDB Connection is Required")) 4763 return false, err 4764 } 4765 4766 if util.LenTrim(d.TableName) <= 0 { 4767 err = d.handleError(errors.New("DynamoDB Table Name is Required")) 4768 return false, err 4769 } 4770 4771 // validate input parameters 4772 if resultItemsPtr == nil { 4773 err = d.handleError(errors.New("DynamoDB BatchGetItems Failed: " + "ResultItems is Nil")) 4774 return false, err 4775 } 4776 4777 if searchKeys == nil { 4778 err = d.handleError(errors.New("DynamoDB BatchGetItems Failed: " + "SearchKeys Cannot Be Nil")) 4779 return false, err 4780 } 4781 4782 if len(searchKeys) <= 0 { 4783 err = d.handleError(errors.New("DynamoDB BatchGetItems Failed: " + "SearchKeys Required")) 4784 return false, err 4785 } 4786 4787 if len(searchKeys) > 100 { 4788 err = d.handleError(errors.New("DynamoDB BatchGetItems Failed: " + "SearchKeys Maximum is 100")) 4789 return false, err 4790 } 4791 4792 trace.Capture("BatchGetItems", func() error { 4793 // marshal search keys into slice of map of dynamodb attribute values 4794 var keysAv []map[string]*dynamodb.AttributeValue 4795 4796 for _, v := range searchKeys { 4797 if m, e := dynamodbattribute.MarshalMap(v); e != nil { 4798 notFound = false 4799 err = d.handleError(e, "DynamoDB BatchGetItems Failed: (SearchKey Marshal)") 4800 return fmt.Errorf(err.ErrorMessage) 4801 } else { 4802 if m != nil { 4803 keysAv = append(keysAv, m) 4804 } else { 4805 notFound = false 4806 err = d.handleError(errors.New("DynamoDB BatchGetItems Failed: (SearchKey Marshal) " + "Marshaled Result Nil")) 4807 return fmt.Errorf(err.ErrorMessage) 4808 } 4809 } 4810 } 4811 4812 // define projected fields 4813 // define projected attributes 4814 var proj expression.ProjectionBuilder 4815 projSet := false 4816 4817 if len(projectedAttributes) > 0 { 4818 // compose projected attributes if specified 4819 firstProjectedAttribute := expression.Name(projectedAttributes[0]) 4820 moreProjectedAttributes := []expression.NameBuilder{} 4821 4822 if len(projectedAttributes) > 1 { 4823 firstAttribute := true 4824 4825 for _, v := range projectedAttributes { 4826 if !firstAttribute { 4827 moreProjectedAttributes = append(moreProjectedAttributes, expression.Name(v)) 4828 } else { 4829 firstAttribute = false 4830 } 4831 } 4832 } 4833 4834 if len(moreProjectedAttributes) > 0 { 4835 proj = expression.NamesList(firstProjectedAttribute, moreProjectedAttributes...) 4836 } else { 4837 proj = expression.NamesList(firstProjectedAttribute) 4838 } 4839 4840 projSet = true 4841 } 4842 4843 var expr expression.Expression 4844 var err1 error 4845 4846 if projSet { 4847 if expr, err1 = expression.NewBuilder().WithProjection(proj).Build(); err1 != nil { 4848 notFound = false 4849 err = d.handleError(err1, "DynamoDB BatchGetItems Failed: (Projecting Attributes)") 4850 return fmt.Errorf(err.ErrorMessage) 4851 } 4852 } 4853 4854 // define params 4855 params := &dynamodb.BatchGetItemInput{ 4856 RequestItems: map[string]*dynamodb.KeysAndAttributes{ 4857 d.TableName: { 4858 Keys: keysAv, 4859 }, 4860 }, 4861 } 4862 4863 if projSet { 4864 params.RequestItems[d.TableName].ProjectionExpression = expr.Projection() 4865 params.RequestItems[d.TableName].ExpressionAttributeNames = expr.Names() 4866 } 4867 4868 if consistentRead != nil { 4869 if *consistentRead { 4870 params.RequestItems[d.TableName].ConsistentRead = consistentRead 4871 } 4872 } 4873 4874 // record params payload 4875 d.LastExecuteParamsPayload = "BatchGetItems = " + params.String() 4876 4877 // execute batch 4878 var result *dynamodb.BatchGetItemOutput 4879 4880 subTrace := trace.NewSubSegment("BatchGetItems_Do") 4881 defer subTrace.Close() 4882 4883 if timeOutDuration != nil { 4884 ctx, cancel := context.WithTimeout(subTrace.Ctx, *timeOutDuration) 4885 defer cancel() 4886 result, err1 = d.do_BatchGetItem(params, ctx) 4887 } else { 4888 result, err1 = d.do_BatchGetItem(params, subTrace.Ctx) 4889 } 4890 4891 // evaluate batch execute result 4892 if err1 != nil { 4893 notFound = false 4894 err = d.handleError(err1, "DynamoDB BatchGetItems Failed: (BatchGetItem)") 4895 return fmt.Errorf(err.ErrorMessage) 4896 } 4897 4898 if result.Responses == nil { 4899 // not found 4900 notFound = true 4901 err = nil 4902 return nil 4903 } else { 4904 // retrieve items found for the given table name 4905 x := result.Responses[d.TableName] 4906 4907 if x == nil { 4908 notFound = true 4909 err = nil 4910 return nil 4911 } else { 4912 // unmarshal results 4913 if err1 = dynamodbattribute.UnmarshalListOfMaps(x, resultItemsPtr); err1 != nil { 4914 notFound = false 4915 err = d.handleError(err1, "DynamoDB BatchGetItems Failed: (Unmarshal ResultItems)") 4916 return fmt.Errorf(err.ErrorMessage) 4917 } else { 4918 // unmarshal successful 4919 notFound = false 4920 err = nil 4921 return nil 4922 } 4923 } 4924 } 4925 }, &xray.XTraceData{ 4926 Meta: map[string]interface{}{ 4927 "TableName": d.TableName, 4928 "SearchKeys": searchKeys, 4929 }, 4930 }) 4931 4932 return notFound, err 4933 } 4934 4935 func (d *DynamoDB) batchGetItemsNormal(resultItemsPtr interface{}, 4936 searchKeys []DynamoDBTableKeys, 4937 timeOutDuration *time.Duration, 4938 consistentRead *bool, 4939 projectedAttributes ...string) (notFound bool, err *DynamoDBError) { 4940 if d.cn == nil { 4941 return false, d.handleError(errors.New("DynamoDB Connection is Required")) 4942 } 4943 4944 if util.LenTrim(d.TableName) <= 0 { 4945 return false, d.handleError(errors.New("DynamoDB Table Name is Required")) 4946 } 4947 4948 // validate input parameters 4949 if resultItemsPtr == nil { 4950 return false, d.handleError(errors.New("DynamoDB BatchGetItems Failed: " + "ResultItems is Nil")) 4951 } 4952 4953 if searchKeys == nil { 4954 return false, d.handleError(errors.New("DynamoDB BatchGetItems Failed: " + "SearchKeys Cannot Be Nil")) 4955 } 4956 4957 if len(searchKeys) <= 0 { 4958 return false, d.handleError(errors.New("DynamoDB BatchGetItems Failed: " + "SearchKeys Required")) 4959 } 4960 4961 if len(searchKeys) > 100 { 4962 return false, d.handleError(errors.New("DynamoDB BatchGetItems Failed: " + "SearchKeys Maximum is 100")) 4963 } 4964 4965 // marshal search keys into slice of map of dynamodb attribute values 4966 var keysAv []map[string]*dynamodb.AttributeValue 4967 4968 for _, v := range searchKeys { 4969 if m, e := dynamodbattribute.MarshalMap(v); e != nil { 4970 notFound = false 4971 err = d.handleError(e, "DynamoDB BatchGetItems Failed: (SearchKey Marshal)") 4972 return notFound, err 4973 } else { 4974 if m != nil { 4975 keysAv = append(keysAv, m) 4976 } else { 4977 notFound = false 4978 err = d.handleError(errors.New("DynamoDB BatchGetItems Failed: (SearchKey Marshal) " + "Marshaled Result Nil")) 4979 return notFound, err 4980 } 4981 } 4982 } 4983 4984 // define projected fields 4985 // define projected attributes 4986 var proj expression.ProjectionBuilder 4987 projSet := false 4988 4989 if len(projectedAttributes) > 0 { 4990 // compose projected attributes if specified 4991 firstProjectedAttribute := expression.Name(projectedAttributes[0]) 4992 moreProjectedAttributes := []expression.NameBuilder{} 4993 4994 if len(projectedAttributes) > 1 { 4995 firstAttribute := true 4996 4997 for _, v := range projectedAttributes { 4998 if !firstAttribute { 4999 moreProjectedAttributes = append(moreProjectedAttributes, expression.Name(v)) 5000 } else { 5001 firstAttribute = false 5002 } 5003 } 5004 } 5005 5006 if len(moreProjectedAttributes) > 0 { 5007 proj = expression.NamesList(firstProjectedAttribute, moreProjectedAttributes...) 5008 } else { 5009 proj = expression.NamesList(firstProjectedAttribute) 5010 } 5011 5012 projSet = true 5013 } 5014 5015 var expr expression.Expression 5016 var err1 error 5017 5018 if projSet { 5019 if expr, err1 = expression.NewBuilder().WithProjection(proj).Build(); err1 != nil { 5020 notFound = false 5021 err = d.handleError(err1, "DynamoDB BatchGetItems Failed: (Projecting Attributes)") 5022 return notFound, err 5023 } 5024 } 5025 5026 // define params 5027 params := &dynamodb.BatchGetItemInput{ 5028 RequestItems: map[string]*dynamodb.KeysAndAttributes{ 5029 d.TableName: { 5030 Keys: keysAv, 5031 }, 5032 }, 5033 } 5034 5035 if projSet { 5036 params.RequestItems[d.TableName].ProjectionExpression = expr.Projection() 5037 params.RequestItems[d.TableName].ExpressionAttributeNames = expr.Names() 5038 } 5039 5040 if consistentRead != nil { 5041 if *consistentRead { 5042 params.RequestItems[d.TableName].ConsistentRead = consistentRead 5043 } 5044 } 5045 5046 // record params payload 5047 d.LastExecuteParamsPayload = "BatchGetItems = " + params.String() 5048 5049 // execute batch 5050 var result *dynamodb.BatchGetItemOutput 5051 5052 if timeOutDuration != nil { 5053 ctx, cancel := context.WithTimeout(context.Background(), *timeOutDuration) 5054 defer cancel() 5055 result, err1 = d.do_BatchGetItem(params, ctx) 5056 } else { 5057 result, err1 = d.do_BatchGetItem(params) 5058 } 5059 5060 // evaluate batch execute result 5061 if err1 != nil { 5062 notFound = false 5063 err = d.handleError(err1, "DynamoDB BatchGetItems Failed: (BatchGetItem)") 5064 return notFound, err 5065 } 5066 5067 if result.Responses == nil { 5068 // not found 5069 return true, nil 5070 } else { 5071 // retrieve items found for the given table name 5072 x := result.Responses[d.TableName] 5073 5074 if x == nil { 5075 return true, nil 5076 } else { 5077 // unmarshal results 5078 if err1 = dynamodbattribute.UnmarshalListOfMaps(x, resultItemsPtr); err1 != nil { 5079 notFound = false 5080 err = d.handleError(err1, "DynamoDB BatchGetItems Failed: (Unmarshal ResultItems)") 5081 return notFound, err 5082 } else { 5083 // unmarshal successful 5084 return false, nil 5085 } 5086 } 5087 } 5088 } 5089 5090 // BatchGetItemsWithRetry handles dynamodb retries in case action temporarily fails 5091 // 5092 // warning 5093 // 5094 // projectedAttributes = if specified, must include PartitionKey (Hash key) typically "PK" as the first attribute in projected attributes 5095 func (d *DynamoDB) BatchGetItemsWithRetry(maxRetries uint, 5096 resultItemsPtr interface{}, 5097 searchKeys []DynamoDBTableKeys, 5098 timeOutDuration *time.Duration, 5099 consistentRead *bool, 5100 projectedAttributes ...string) (notFound bool, err *DynamoDBError) { 5101 if maxRetries > 10 { 5102 maxRetries = 10 5103 } 5104 5105 timeout := 5 * time.Second 5106 5107 if timeOutDuration != nil { 5108 timeout = *timeOutDuration 5109 } 5110 5111 if timeout < 5*time.Second { 5112 timeout = 5 * time.Second 5113 } else if timeout > 15*time.Second { 5114 timeout = 15 * time.Second 5115 } 5116 5117 if notFound, err = d.BatchGetItems(resultItemsPtr, searchKeys, util.DurationPtr(timeout), consistentRead, projectedAttributes...); err != nil { 5118 // has error 5119 if maxRetries > 0 { 5120 if err.AllowRetry { 5121 if err.RetryNeedsBackOff { 5122 time.Sleep(500 * time.Millisecond) 5123 } else { 5124 time.Sleep(100 * time.Millisecond) 5125 } 5126 5127 log.Println("BatchGetItemsWithRetry Failed: " + err.ErrorMessage) 5128 return d.BatchGetItemsWithRetry(maxRetries-1, resultItemsPtr, searchKeys, util.DurationPtr(timeout), consistentRead, projectedAttributes...) 5129 } else { 5130 if err.SuppressError { 5131 log.Println("BatchGetItemsWithRetry DynamoDB Error Suppressed, Returning Error Nil (MaxRetries = " + util.UintToStr(maxRetries) + ")") 5132 return true, nil 5133 } else { 5134 return true, &DynamoDBError{ 5135 ErrorMessage: "BatchGetItemsWithRetry Failed: " + err.ErrorMessage, 5136 SuppressError: false, 5137 AllowRetry: false, 5138 RetryNeedsBackOff: false, 5139 } 5140 } 5141 } 5142 } else { 5143 if err.SuppressError { 5144 log.Println("BatchGetItemsWithRetry DynamoDB Error Suppressed, Returning Error Nil (MaxRetries = 0)") 5145 return true, nil 5146 } else { 5147 return true, &DynamoDBError{ 5148 ErrorMessage: "BatchGetItemsWithRetry Failed: (MaxRetries = 0) " + err.ErrorMessage, 5149 SuppressError: false, 5150 AllowRetry: false, 5151 RetryNeedsBackOff: false, 5152 } 5153 } 5154 } 5155 } else { 5156 // no error 5157 return notFound, nil 5158 } 5159 } 5160 5161 // BatchDeleteItemsWithRetry will attempt to delete one or more table records by on deleteKeys input, 5162 // will auto retry delete if temporarily failed, 5163 // if there are deleteFailKeys, its returned, if all succeeded, nil is returned 5164 func (d *DynamoDB) BatchDeleteItemsWithRetry(maxRetries uint, 5165 timeOutDuration *time.Duration, 5166 deleteKeys ...*DynamoDBTableKeys) (deleteFailKeys []*DynamoDBTableKeys, err error) { 5167 if len(deleteKeys) == 0 { 5168 return []*DynamoDBTableKeys{}, fmt.Errorf("BatchDeleteItemsWithRetry Failed: %s", err) 5169 } 5170 5171 if maxRetries > 10 { 5172 maxRetries = 10 5173 } 5174 5175 timeout := 5 * time.Second 5176 5177 if timeOutDuration != nil { 5178 timeout = *timeOutDuration 5179 } 5180 5181 if timeout < 5*time.Second { 5182 timeout = 5 * time.Second 5183 } else if timeout > 15*time.Second { 5184 timeout = 15 * time.Second 5185 } 5186 5187 for _, key := range deleteKeys { 5188 if key != nil && util.LenTrim(key.PK) > 0 { 5189 retries := maxRetries 5190 5191 if e := d.DeleteItemWithRetry(retries, key.PK, key.SK, util.DurationPtr(timeout)); e != nil { 5192 key.ResultError = e 5193 deleteFailKeys = append(deleteFailKeys, key) 5194 } 5195 } 5196 } 5197 5198 if len(deleteFailKeys) == len(deleteKeys) { 5199 // all failed 5200 return deleteFailKeys, fmt.Errorf("BatchDeleteItemsWithRetry Failed: All Delete Actions Failed") 5201 5202 } else if len(deleteFailKeys) == 0 { 5203 // all success 5204 return []*DynamoDBTableKeys{}, nil 5205 5206 } else { 5207 // some failed 5208 return deleteFailKeys, fmt.Errorf("BatchDeleteItemsWithRetry Partial Failure: Some Delete Actions Failed") 5209 } 5210 } 5211 5212 // TransactionWriteItems performs a transaction write action for one or more DynamoDBTransactionWrites struct objects, 5213 // Either all success or all fail, 5214 // Total Items Count in a Single Transaction for All transItems combined (inner elements) cannot exceed 25 5215 // 5216 // important 5217 // 5218 // if dynamodb table is defined as PK and SK together, then to search, MUST use PK and SK together or error will trigger 5219 func (d *DynamoDB) TransactionWriteItems(timeOutDuration *time.Duration, tranItems ...*DynamoDBTransactionWrites) (success bool, err *DynamoDBError) { 5220 if xray.XRayServiceOn() { 5221 return d.transactionWriteItemsWithTrace(timeOutDuration, tranItems...) 5222 } else { 5223 return d.transactionWriteItemsNormal(timeOutDuration, tranItems...) 5224 } 5225 } 5226 5227 func (d *DynamoDB) transactionWriteItemsWithTrace(timeOutDuration *time.Duration, tranItems ...*DynamoDBTransactionWrites) (success bool, err *DynamoDBError) { 5228 trace := xray.NewSegment("DynamoDB-TransactionWriteItems", d._parentSegment) 5229 defer trace.Close() 5230 defer func() { 5231 if err != nil { 5232 _ = trace.Seg.AddError(fmt.Errorf(err.ErrorMessage)) 5233 } 5234 }() 5235 5236 if d.cn == nil { 5237 err = d.handleError(errors.New("DynamoDB Connection is Required")) 5238 return false, err 5239 } 5240 5241 if util.LenTrim(d.TableName) <= 0 { 5242 err = d.handleError(errors.New("DynamoDB Table Name is Required")) 5243 return false, err 5244 } 5245 5246 if util.LenTrim(d.PKName) <= 0 { 5247 err = d.handleError(errors.New("DynamoDB TransactionWriteItems Failed: " + "PK Name is Required")) 5248 return false, err 5249 } 5250 5251 if len(tranItems) == 0 { 5252 err = d.handleError(errors.New("DynamoDB TransactionWriteItems Failed: " + "Minimum of 1 TranItems is Required")) 5253 return false, err 5254 } 5255 5256 trace.Capture("TransactionWriteItems", func() error { 5257 // create working data 5258 var items []*dynamodb.TransactWriteItem 5259 5260 // loop through all tranItems slice to pre-populate transaction write items slice 5261 skOK := false 5262 5263 for _, t := range tranItems { 5264 tableName := t.TableNameOverride 5265 5266 if util.LenTrim(tableName) <= 0 { 5267 tableName = d.TableName 5268 } 5269 5270 if t.DeleteItems != nil && len(t.DeleteItems) > 0 { 5271 for _, v := range t.DeleteItems { 5272 m := new(dynamodb.TransactWriteItem) 5273 5274 md := make(map[string]*dynamodb.AttributeValue) 5275 md[d.PKName] = &dynamodb.AttributeValue{S: aws.String(v.PK)} 5276 5277 if util.LenTrim(v.SK) > 0 { 5278 if !skOK { 5279 if util.LenTrim(d.SKName) <= 0 { 5280 success = false 5281 err = d.handleError(errors.New("DynamoDB TransactionWriteItems Failed: (Payload Validate) " + "SK Name is Required")) 5282 return fmt.Errorf(err.ErrorMessage) 5283 } else { 5284 skOK = true 5285 } 5286 } 5287 5288 md[d.SKName] = &dynamodb.AttributeValue{S: aws.String(v.SK)} 5289 } 5290 5291 m.Delete = &dynamodb.Delete{ 5292 TableName: aws.String(tableName), 5293 Key: md, 5294 } 5295 5296 items = append(items, m) 5297 } 5298 } 5299 5300 if t.PutItems != nil { 5301 if md, e := t.MarshalPutItems(); e != nil { 5302 success = false 5303 err = d.handleError(e, "DynamoDB TransactionWriteItems Failed: (Marshal PutItems)") 5304 return fmt.Errorf(err.ErrorMessage) 5305 } else { 5306 for _, v := range md { 5307 m := new(dynamodb.TransactWriteItem) 5308 5309 m.Put = &dynamodb.Put{ 5310 TableName: aws.String(tableName), 5311 Item: v, 5312 } 5313 5314 items = append(items, m) 5315 } 5316 } 5317 } 5318 5319 if t.UpdateItems != nil && len(t.UpdateItems) > 0 { 5320 for _, v := range t.UpdateItems { 5321 m := new(dynamodb.TransactWriteItem) 5322 5323 mk := make(map[string]*dynamodb.AttributeValue) 5324 mk[d.PKName] = &dynamodb.AttributeValue{S: aws.String(v.PK)} 5325 5326 if util.LenTrim(v.SK) > 0 { 5327 if !skOK { 5328 if util.LenTrim(d.SKName) <= 0 { 5329 success = false 5330 err = d.handleError(errors.New("DynamoDB TransactionWriteItems Failed: (Payload Validate) " + "SK Name is Required")) 5331 return fmt.Errorf(err.ErrorMessage) 5332 } else { 5333 skOK = true 5334 } 5335 } 5336 5337 mk[d.SKName] = &dynamodb.AttributeValue{S: aws.String(v.SK)} 5338 } 5339 5340 m.Update = &dynamodb.Update{ 5341 TableName: aws.String(tableName), 5342 Key: mk, 5343 } 5344 5345 if util.LenTrim(v.ConditionExpression) > 0 { 5346 m.Update.ConditionExpression = aws.String(v.ConditionExpression) 5347 } 5348 5349 if util.LenTrim(v.UpdateExpression) > 0 { 5350 m.Update.UpdateExpression = aws.String(v.UpdateExpression) 5351 } 5352 5353 if v.ExpressionAttributeNames != nil && len(v.ExpressionAttributeNames) > 0 { 5354 m.Update.ExpressionAttributeNames = v.ExpressionAttributeNames 5355 } 5356 5357 if v.ExpressionAttributeValues != nil && len(v.ExpressionAttributeValues) > 0 { 5358 m.Update.ExpressionAttributeValues = v.ExpressionAttributeValues 5359 } 5360 5361 items = append(items, m) 5362 } 5363 } 5364 } 5365 5366 // items must not exceed 25 5367 if len(items) > 25 { 5368 success = false 5369 err = d.handleError(errors.New("DynamoDB TransactionWriteItems Failed: (Payload Validate) " + "Transaction Items May Not Exceed 25")) 5370 return fmt.Errorf(err.ErrorMessage) 5371 } 5372 5373 if len(items) <= 0 { 5374 success = false 5375 err = d.handleError(errors.New("DynamoDB TransactionWriteItems Failed: (Payload Validate) " + "Transaction Items Minimum of 1 is Required")) 5376 return fmt.Errorf(err.ErrorMessage) 5377 } 5378 5379 // compose transaction write items input var 5380 params := &dynamodb.TransactWriteItemsInput{ 5381 TransactItems: items, 5382 } 5383 5384 // record params payload 5385 d.LastExecuteParamsPayload = "TransactionWriteItems = " + params.String() 5386 5387 // execute transaction write operation 5388 var err1 error 5389 5390 subTrace := trace.NewSubSegment("TransactionWriteItems_Do") 5391 defer subTrace.Close() 5392 5393 if timeOutDuration != nil { 5394 ctx, cancel := context.WithTimeout(subTrace.Ctx, *timeOutDuration) 5395 defer cancel() 5396 _, err1 = d.do_TransactWriteItems(params, ctx) 5397 } else { 5398 _, err1 = d.do_TransactWriteItems(params, subTrace.Ctx) 5399 } 5400 5401 if err1 != nil { 5402 success = false 5403 err = d.handleError(err1, "DynamoDB TransactionWriteItems Failed: (Transaction Canceled)") 5404 return fmt.Errorf(err.ErrorMessage) 5405 } else { 5406 success = true 5407 err = nil 5408 return nil 5409 } 5410 }, &xray.XTraceData{ 5411 Meta: map[string]interface{}{ 5412 "TableName": d.TableName, 5413 "Items": tranItems, 5414 }, 5415 }) 5416 5417 // success 5418 return success, err 5419 } 5420 5421 func (d *DynamoDB) transactionWriteItemsNormal(timeOutDuration *time.Duration, tranItems ...*DynamoDBTransactionWrites) (success bool, err *DynamoDBError) { 5422 if d.cn == nil { 5423 return false, d.handleError(errors.New("DynamoDB Connection is Required")) 5424 } 5425 5426 if util.LenTrim(d.TableName) <= 0 { 5427 return false, d.handleError(errors.New("DynamoDB Table Name is Required")) 5428 } 5429 5430 if util.LenTrim(d.PKName) <= 0 { 5431 return false, d.handleError(errors.New("DynamoDB TransactionWriteItems Failed: " + "PK Name is Required")) 5432 } 5433 5434 if len(tranItems) == 0 { 5435 return false, d.handleError(errors.New("DynamoDB TransactionWriteItems Failed: " + "Minimum of 1 TranItems is Required")) 5436 } 5437 5438 // create working data 5439 var items []*dynamodb.TransactWriteItem 5440 5441 // loop through all tranItems slice to pre-populate transaction write items slice 5442 skOK := false 5443 5444 for _, t := range tranItems { 5445 tableName := t.TableNameOverride 5446 5447 if util.LenTrim(tableName) <= 0 { 5448 tableName = d.TableName 5449 } 5450 5451 if t.DeleteItems != nil && len(t.DeleteItems) > 0 { 5452 for _, v := range t.DeleteItems { 5453 m := new(dynamodb.TransactWriteItem) 5454 5455 md := make(map[string]*dynamodb.AttributeValue) 5456 md[d.PKName] = &dynamodb.AttributeValue{S: aws.String(v.PK)} 5457 5458 if util.LenTrim(v.SK) > 0 { 5459 if !skOK { 5460 if util.LenTrim(d.SKName) <= 0 { 5461 success = false 5462 err = d.handleError(errors.New("DynamoDB TransactionWriteItems Failed: (Payload Validate) " + "SK Name is Required")) 5463 return success, err 5464 } else { 5465 skOK = true 5466 } 5467 } 5468 5469 md[d.SKName] = &dynamodb.AttributeValue{S: aws.String(v.SK)} 5470 } 5471 5472 m.Delete = &dynamodb.Delete{ 5473 TableName: aws.String(tableName), 5474 Key: md, 5475 } 5476 5477 items = append(items, m) 5478 } 5479 } 5480 5481 if t.PutItems != nil { 5482 if md, e := t.MarshalPutItems(); e != nil { 5483 success = false 5484 err = d.handleError(e, "DynamoDB TransactionWriteItems Failed: (Marshal PutItems)") 5485 return success, err 5486 } else { 5487 for _, v := range md { 5488 m := new(dynamodb.TransactWriteItem) 5489 5490 m.Put = &dynamodb.Put{ 5491 TableName: aws.String(tableName), 5492 Item: v, 5493 } 5494 5495 items = append(items, m) 5496 } 5497 } 5498 } 5499 5500 if t.UpdateItems != nil && len(t.UpdateItems) > 0 { 5501 for _, v := range t.UpdateItems { 5502 m := new(dynamodb.TransactWriteItem) 5503 5504 mk := make(map[string]*dynamodb.AttributeValue) 5505 mk[d.PKName] = &dynamodb.AttributeValue{S: aws.String(v.PK)} 5506 5507 if util.LenTrim(v.SK) > 0 { 5508 if !skOK { 5509 if util.LenTrim(d.SKName) <= 0 { 5510 success = false 5511 err = d.handleError(errors.New("DynamoDB TransactionWriteItems Failed: (Payload Validate) " + "SK Name is Required")) 5512 return success, err 5513 } else { 5514 skOK = true 5515 } 5516 } 5517 5518 mk[d.SKName] = &dynamodb.AttributeValue{S: aws.String(v.SK)} 5519 } 5520 5521 m.Update = &dynamodb.Update{ 5522 TableName: aws.String(tableName), 5523 Key: mk, 5524 } 5525 5526 if util.LenTrim(v.ConditionExpression) > 0 { 5527 m.Update.ConditionExpression = aws.String(v.ConditionExpression) 5528 } 5529 5530 if util.LenTrim(v.UpdateExpression) > 0 { 5531 m.Update.UpdateExpression = aws.String(v.UpdateExpression) 5532 } 5533 5534 if v.ExpressionAttributeNames != nil && len(v.ExpressionAttributeNames) > 0 { 5535 m.Update.ExpressionAttributeNames = v.ExpressionAttributeNames 5536 } 5537 5538 if v.ExpressionAttributeValues != nil && len(v.ExpressionAttributeValues) > 0 { 5539 m.Update.ExpressionAttributeValues = v.ExpressionAttributeValues 5540 } 5541 5542 items = append(items, m) 5543 } 5544 } 5545 } 5546 5547 // items must not exceed 25 5548 if len(items) > 25 { 5549 success = false 5550 err = d.handleError(errors.New("DynamoDB TransactionWriteItems Failed: (Payload Validate) " + "Transaction Items May Not Exceed 25")) 5551 return success, err 5552 } 5553 5554 if len(items) <= 0 { 5555 success = false 5556 err = d.handleError(errors.New("DynamoDB TransactionWriteItems Failed: (Payload Validate) " + "Transaction Items Minimum of 1 is Required")) 5557 return success, err 5558 } 5559 5560 // compose transaction write items input var 5561 params := &dynamodb.TransactWriteItemsInput{ 5562 TransactItems: items, 5563 } 5564 5565 // record params payload 5566 d.LastExecuteParamsPayload = "TransactionWriteItems = " + params.String() 5567 5568 // execute transaction write operation 5569 var err1 error 5570 5571 if timeOutDuration != nil { 5572 ctx, cancel := context.WithTimeout(context.Background(), *timeOutDuration) 5573 defer cancel() 5574 _, err1 = d.do_TransactWriteItems(params, ctx) 5575 } else { 5576 _, err1 = d.do_TransactWriteItems(params) 5577 } 5578 5579 if err1 != nil { 5580 success = false 5581 err = d.handleError(err1, "DynamoDB TransactionWriteItems Failed: (Transaction Canceled)") 5582 return success, err 5583 } else { 5584 return true, nil 5585 } 5586 } 5587 5588 // TransactionWriteItemsWithRetry handles dynamodb retries in case action temporarily fails 5589 func (d *DynamoDB) TransactionWriteItemsWithRetry(maxRetries uint, 5590 timeOutDuration *time.Duration, 5591 tranItems ...*DynamoDBTransactionWrites) (success bool, err *DynamoDBError) { 5592 if maxRetries > 10 { 5593 maxRetries = 10 5594 } 5595 5596 timeout := 10 * time.Second 5597 5598 if timeOutDuration != nil { 5599 timeout = *timeOutDuration 5600 } 5601 5602 if timeout < 10*time.Second { 5603 timeout = 10 * time.Second 5604 } else if timeout > 30*time.Second { 5605 timeout = 30 * time.Second 5606 } 5607 5608 if success, err = d.TransactionWriteItems(util.DurationPtr(timeout), tranItems...); err != nil { 5609 // has error 5610 if maxRetries > 0 { 5611 if err.AllowRetry { 5612 if err.RetryNeedsBackOff { 5613 time.Sleep(500 * time.Millisecond) 5614 } else { 5615 time.Sleep(100 * time.Millisecond) 5616 } 5617 5618 log.Println("TransactionWriteItemsWithRetry Failed: " + err.ErrorMessage) 5619 return d.TransactionWriteItemsWithRetry(maxRetries-1, util.DurationPtr(timeout), tranItems...) 5620 } else { 5621 if err.SuppressError { 5622 log.Println("TransactionWriteItemsWithRetry DynamoDB Error Suppressed, Returning Error Nil (MaxRetries = " + util.UintToStr(maxRetries) + ")") 5623 return false, nil 5624 } else { 5625 return false, &DynamoDBError{ 5626 ErrorMessage: "TransactionWriteItemsWithRetry Failed: " + err.ErrorMessage, 5627 SuppressError: false, 5628 AllowRetry: false, 5629 RetryNeedsBackOff: false, 5630 } 5631 } 5632 } 5633 } else { 5634 if err.SuppressError { 5635 log.Println("TransactionWriteItemsWithRetry DynamoDB Error Suppressed, Returning Error Nil (MaxRetries = 0)") 5636 return false, nil 5637 } else { 5638 return false, &DynamoDBError{ 5639 ErrorMessage: "TransactionWriteItemsWithRetry Failed: (MaxRetries = 0) " + err.ErrorMessage, 5640 SuppressError: false, 5641 AllowRetry: false, 5642 RetryNeedsBackOff: false, 5643 } 5644 } 5645 } 5646 } else { 5647 // no error 5648 return success, nil 5649 } 5650 } 5651 5652 // TransactionGetItems receives parameters via tranKeys variadic objects of type DynamoDBTransactionReads; each object has TableName override in case querying against other tables 5653 // Each tranKeys struct object can contain one or more DynamoDBTableKeys struct, which contains PK, SK fields, and ResultItemPtr, 5654 // The PK (required) and SK (optional) is used for search, while ResultItemPtr interface{} receives pointer to the output object, so that once query completes the appropriate item data will unmarshal into object 5655 // 5656 // important 5657 // 5658 // if dynamodb table is defined as PK and SK together, then to search, MUST use PK and SK together or error will trigger 5659 // 5660 // setting result item ptr info 5661 // 1. Each DynamoDBTableKeys struct object must set pointer of output struct object to ResultItemPtr 5662 // 2. In the external calling code, must define slice of struct object pointers to receive such unmarshaled results 5663 // a) output := []*MID{ 5664 // &MID{}, 5665 // &MID{}, 5666 // } 5667 // b) Usage 5668 // Passing each element of output to ResultItemPtr within DynamoDBTableKeys struct object 5669 // 5670 // notes: 5671 // 1. transKeys' must contain at laest one object 5672 // 2. within transKeys object, at least one object of DynamoDBTableKeys must exist for search 5673 // 3. no more than total of 25 search keys allowed across all variadic objects 5674 // 4. the ResultItemPtr in all DynamoDBTableKeys objects within all variadic objects MUST BE SET 5675 func (d *DynamoDB) TransactionGetItems(timeOutDuration *time.Duration, tranKeys ...*DynamoDBTransactionReads) (successCount int, err *DynamoDBError) { 5676 if xray.XRayServiceOn() { 5677 return d.transactionGetItemsWithTrace(timeOutDuration, tranKeys...) 5678 } else { 5679 return d.transactionGetItemsNormal(timeOutDuration, tranKeys...) 5680 } 5681 } 5682 5683 func (d *DynamoDB) transactionGetItemsWithTrace(timeOutDuration *time.Duration, tranKeys ...*DynamoDBTransactionReads) (successCount int, err *DynamoDBError) { 5684 trace := xray.NewSegment("DynamoDB-TransactionGetItems", d._parentSegment) 5685 defer trace.Close() 5686 defer func() { 5687 if err != nil { 5688 _ = trace.Seg.AddError(fmt.Errorf(err.ErrorMessage)) 5689 } 5690 }() 5691 5692 if d.cn == nil { 5693 err = d.handleError(errors.New("DynamoDB Connection is Required")) 5694 return 0, err 5695 } 5696 5697 if util.LenTrim(d.TableName) <= 0 { 5698 err = d.handleError(errors.New("DynamoDB Table Name is Required")) 5699 return 0, err 5700 } 5701 5702 if util.LenTrim(d.PKName) <= 0 { 5703 err = d.handleError(errors.New("DynamoDB TransactionGetItems Failed: " + "PK Name is Required")) 5704 return 0, err 5705 } 5706 5707 if len(tranKeys) == 0 { 5708 err = d.handleError(errors.New("DynamoDB TransactionGetItems Failed: " + "Minimum of 1 TranKeys is Required")) 5709 return 0, err 5710 } 5711 5712 trace.Capture("TransactionGetItems", func() error { 5713 // create working data 5714 var keys []*dynamodb.TransactGetItem 5715 var output []*DynamoDBTableKeys 5716 5717 // loop through all tranKeys slice to pre-populate transaction get items key slice 5718 skOK := false 5719 5720 for _, k := range tranKeys { 5721 tableName := k.TableNameOverride 5722 5723 if util.LenTrim(tableName) <= 0 { 5724 tableName = d.TableName 5725 } 5726 5727 if k.Keys != nil && len(k.Keys) > 0 { 5728 for _, v := range k.Keys { 5729 if v.ResultItemPtr == nil { 5730 successCount = 0 5731 err = d.handleError(errors.New("DynamoDB TransactionGetItems Failed: " + "All SearchKeys Must Define Unmarshal Target Object")) 5732 return fmt.Errorf(err.ErrorMessage) 5733 } else { 5734 // add to output 5735 output = append(output, v) 5736 } 5737 5738 m := new(dynamodb.TransactGetItem) 5739 5740 md := make(map[string]*dynamodb.AttributeValue) 5741 md[d.PKName] = &dynamodb.AttributeValue{S: aws.String(v.PK)} 5742 5743 if util.LenTrim(v.SK) > 0 { 5744 if !skOK { 5745 if util.LenTrim(d.SKName) <= 0 { 5746 successCount = 0 5747 err = d.handleError(errors.New("DynamoDB TransactionGetItems Failed: " + "SK Name is Required")) 5748 return fmt.Errorf(err.ErrorMessage) 5749 } else { 5750 skOK = true 5751 } 5752 } 5753 5754 md[d.SKName] = &dynamodb.AttributeValue{S: aws.String(v.SK)} 5755 } 5756 5757 m.Get = &dynamodb.Get{ 5758 TableName: aws.String(tableName), 5759 Key: md, 5760 } 5761 5762 keys = append(keys, m) 5763 } 5764 } 5765 } 5766 5767 // keys must not exceed 25 5768 if len(keys) > 25 { 5769 successCount = 0 5770 err = d.handleError(errors.New("DynamoDB TransactionGetItems Failed: (Payload Validate) " + "Search Keys May Not Exceed 25")) 5771 return fmt.Errorf(err.ErrorMessage) 5772 } 5773 5774 if len(keys) <= 0 { 5775 successCount = 0 5776 err = d.handleError(errors.New("DynamoDB TransactionGetItems Failed: (Payload Validate) " + "Search Keys Minimum of 1 is Required")) 5777 return fmt.Errorf(err.ErrorMessage) 5778 } 5779 5780 // compose transaction get items input var 5781 params := &dynamodb.TransactGetItemsInput{ 5782 TransactItems: keys, 5783 } 5784 5785 // record params payload 5786 d.LastExecuteParamsPayload = "TransactionGetItems = " + params.String() 5787 5788 // execute transaction get operation 5789 var result *dynamodb.TransactGetItemsOutput 5790 var err1 error 5791 5792 subTrace := trace.NewSubSegment("TransactionGetItems_Do") 5793 defer subTrace.Close() 5794 5795 if timeOutDuration != nil { 5796 ctx, cancel := context.WithTimeout(subTrace.Ctx, *timeOutDuration) 5797 defer cancel() 5798 result, err1 = d.do_TransactGetItems(params, ctx) 5799 } else { 5800 result, err1 = d.do_TransactGetItems(params, subTrace.Ctx) 5801 } 5802 5803 if err1 != nil { 5804 successCount = 0 5805 err = d.handleError(err1, "DynamoDB TransactionGetItems Failed: (Transaction Reads)") 5806 return fmt.Errorf(err.ErrorMessage) 5807 } 5808 5809 // evaluate response 5810 successCount = 0 5811 5812 if result.Responses != nil && len(result.Responses) > 0 { 5813 hasSK := util.LenTrim(d.SKName) > 0 5814 5815 for i, v := range result.Responses { 5816 itemAv := v.Item 5817 5818 if itemAv != nil { 5819 pk := util.Trim(aws.StringValue(itemAv[d.PKName].S)) 5820 sk := "" 5821 5822 if hasSK { 5823 sk = util.Trim(aws.StringValue(itemAv[d.SKName].S)) 5824 } 5825 5826 if len(pk) > 0 { 5827 if i < len(output) { 5828 if o := output[i]; o != nil && !o.resultProcessed { 5829 found := false 5830 5831 if len(sk) > 0 { 5832 // must match pk and sk 5833 if o.PK == pk && o.SK == sk && o.ResultItemPtr != nil { 5834 found = true 5835 } 5836 } else { 5837 // must match pk only 5838 if o.PK == pk && o.ResultItemPtr != nil { 5839 found = true 5840 } 5841 } 5842 5843 if found { 5844 o.resultProcessed = true 5845 5846 // unmarshal to object 5847 if e := dynamodbattribute.UnmarshalMap(itemAv, o.ResultItemPtr); e != nil { 5848 successCount = 0 5849 err = d.handleError(e, "DynamoDB TransactionGetItems Failed: (Unmarshal Result)") 5850 return fmt.Errorf(err.ErrorMessage) 5851 } else { 5852 // unmarshal successful 5853 successCount++ 5854 } 5855 } 5856 } 5857 } 5858 } 5859 } 5860 } 5861 } 5862 5863 err = nil 5864 return nil 5865 }, &xray.XTraceData{ 5866 Meta: map[string]interface{}{ 5867 "TableName": d.TableName, 5868 "Keys": tranKeys, 5869 }, 5870 }) 5871 5872 // nothing found or something found, both returns nil for error 5873 return successCount, err 5874 } 5875 5876 func (d *DynamoDB) transactionGetItemsNormal(timeOutDuration *time.Duration, tranKeys ...*DynamoDBTransactionReads) (successCount int, err *DynamoDBError) { 5877 if d.cn == nil { 5878 return 0, d.handleError(errors.New("DynamoDB Connection is Required")) 5879 } 5880 5881 if util.LenTrim(d.TableName) <= 0 { 5882 return 0, d.handleError(errors.New("DynamoDB Table Name is Required")) 5883 } 5884 5885 if util.LenTrim(d.PKName) <= 0 { 5886 return 0, d.handleError(errors.New("DynamoDB TransactionGetItems Failed: " + "PK Name is Required")) 5887 } 5888 5889 if len(tranKeys) == 0 { 5890 return 0, d.handleError(errors.New("DynamoDB TransactionGetItems Failed: " + "Minimum of 1 TranKeys is Required")) 5891 } 5892 5893 // create working data 5894 var keys []*dynamodb.TransactGetItem 5895 var output []*DynamoDBTableKeys 5896 5897 // loop through all tranKeys slice to pre-populate transaction get items key slice 5898 skOK := false 5899 5900 for _, k := range tranKeys { 5901 tableName := k.TableNameOverride 5902 5903 if util.LenTrim(tableName) <= 0 { 5904 tableName = d.TableName 5905 } 5906 5907 if k.Keys != nil && len(k.Keys) > 0 { 5908 for _, v := range k.Keys { 5909 if v.ResultItemPtr == nil { 5910 successCount = 0 5911 err = d.handleError(errors.New("DynamoDB TransactionGetItems Failed: " + "All SearchKeys Must Define Unmarshal Target Object")) 5912 return successCount, err 5913 } else { 5914 // add to output 5915 output = append(output, v) 5916 } 5917 5918 m := new(dynamodb.TransactGetItem) 5919 5920 md := make(map[string]*dynamodb.AttributeValue) 5921 md[d.PKName] = &dynamodb.AttributeValue{S: aws.String(v.PK)} 5922 5923 if util.LenTrim(v.SK) > 0 { 5924 if !skOK { 5925 if util.LenTrim(d.SKName) <= 0 { 5926 successCount = 0 5927 err = d.handleError(errors.New("DynamoDB TransactionGetItems Failed: " + "SK Name is Required")) 5928 return successCount, err 5929 } else { 5930 skOK = true 5931 } 5932 } 5933 5934 md[d.SKName] = &dynamodb.AttributeValue{S: aws.String(v.SK)} 5935 } 5936 5937 m.Get = &dynamodb.Get{ 5938 TableName: aws.String(tableName), 5939 Key: md, 5940 } 5941 5942 keys = append(keys, m) 5943 } 5944 } 5945 } 5946 5947 // keys must not exceed 25 5948 if len(keys) > 25 { 5949 successCount = 0 5950 err = d.handleError(errors.New("DynamoDB TransactionGetItems Failed: (Payload Validate) " + "Search Keys May Not Exceed 25")) 5951 return successCount, err 5952 } 5953 5954 if len(keys) <= 0 { 5955 successCount = 0 5956 err = d.handleError(errors.New("DynamoDB TransactionGetItems Failed: (Payload Validate) " + "Search Keys Minimum of 1 is Required")) 5957 return successCount, err 5958 } 5959 5960 // compose transaction get items input var 5961 params := &dynamodb.TransactGetItemsInput{ 5962 TransactItems: keys, 5963 } 5964 5965 // record params payload 5966 d.LastExecuteParamsPayload = "TransactionGetItems = " + params.String() 5967 5968 // execute transaction get operation 5969 var result *dynamodb.TransactGetItemsOutput 5970 var err1 error 5971 5972 if timeOutDuration != nil { 5973 ctx, cancel := context.WithTimeout(context.Background(), *timeOutDuration) 5974 defer cancel() 5975 result, err1 = d.do_TransactGetItems(params, ctx) 5976 } else { 5977 result, err1 = d.do_TransactGetItems(params) 5978 } 5979 5980 if err1 != nil { 5981 successCount = 0 5982 err = d.handleError(err1, "DynamoDB TransactionGetItems Failed: (Transaction Reads)") 5983 return successCount, err 5984 } 5985 5986 // evaluate response 5987 successCount = 0 5988 5989 if result.Responses != nil && len(result.Responses) > 0 { 5990 hasSK := util.LenTrim(d.SKName) > 0 5991 5992 for i, v := range result.Responses { 5993 itemAv := v.Item 5994 5995 if itemAv != nil { 5996 pk := util.Trim(aws.StringValue(itemAv[d.PKName].S)) 5997 sk := "" 5998 5999 if hasSK { 6000 sk = util.Trim(aws.StringValue(itemAv[d.SKName].S)) 6001 } 6002 6003 if len(pk) > 0 { 6004 if i < len(output) { 6005 if o := output[i]; o != nil && !o.resultProcessed { 6006 found := false 6007 6008 if len(sk) > 0 { 6009 // must match pk and sk 6010 if o.PK == pk && o.SK == sk && o.ResultItemPtr != nil { 6011 found = true 6012 } 6013 } else { 6014 // must match pk only 6015 if o.PK == pk && o.ResultItemPtr != nil { 6016 found = true 6017 } 6018 } 6019 6020 if found { 6021 o.resultProcessed = true 6022 6023 // unmarshal to object 6024 if e := dynamodbattribute.UnmarshalMap(itemAv, o.ResultItemPtr); e != nil { 6025 successCount = 0 6026 err = d.handleError(e, "DynamoDB TransactionGetItems Failed: (Unmarshal Result)") 6027 return successCount, err 6028 } else { 6029 // unmarshal successful 6030 successCount++ 6031 } 6032 } 6033 } 6034 } 6035 } 6036 } 6037 } 6038 } 6039 6040 // nothing found or something found, both returns nil for error 6041 return successCount, nil 6042 } 6043 6044 // TransactionGetItemsWithRetry handles dynamodb retries in case action temporarily fails 6045 func (d *DynamoDB) TransactionGetItemsWithRetry(maxRetries uint, 6046 timeOutDuration *time.Duration, 6047 tranKeys ...*DynamoDBTransactionReads) (successCount int, err *DynamoDBError) { 6048 if maxRetries > 10 { 6049 maxRetries = 10 6050 } 6051 6052 timeout := 5 * time.Second 6053 6054 if timeOutDuration != nil { 6055 timeout = *timeOutDuration 6056 } 6057 6058 if timeout < 5*time.Second { 6059 timeout = 5 * time.Second 6060 } else if timeout > 15*time.Second { 6061 timeout = 15 * time.Second 6062 } 6063 6064 if successCount, err = d.TransactionGetItems(util.DurationPtr(timeout), tranKeys...); err != nil { 6065 // has error 6066 if maxRetries > 0 { 6067 if err.AllowRetry { 6068 if err.RetryNeedsBackOff { 6069 time.Sleep(500 * time.Millisecond) 6070 } else { 6071 time.Sleep(100 * time.Millisecond) 6072 } 6073 6074 log.Println("TransactionGetItemsWithRetry Failed: " + err.ErrorMessage) 6075 return d.TransactionGetItemsWithRetry(maxRetries-1, util.DurationPtr(timeout), tranKeys...) 6076 } else { 6077 if err.SuppressError { 6078 log.Println("TransactionGetItemsWithRetry DynamoDB Error Suppressed, Returning Error Nil (MaxRetries = " + util.UintToStr(maxRetries) + ")") 6079 return 0, nil 6080 } else { 6081 return 0, &DynamoDBError{ 6082 ErrorMessage: "TransactionGetItemsWithRetry Failed: " + err.ErrorMessage, 6083 SuppressError: false, 6084 AllowRetry: false, 6085 RetryNeedsBackOff: false, 6086 } 6087 } 6088 } 6089 } else { 6090 if err.SuppressError { 6091 log.Println("TransactionGetItemsWithRetry DynamoDB Error Suppressed, Returning Error Nil (MaxRetries = 0)") 6092 return 0, nil 6093 } else { 6094 return 0, &DynamoDBError{ 6095 ErrorMessage: "TransactionGetItemsWithRetry Failed: (MaxRetries = 0) " + err.ErrorMessage, 6096 SuppressError: false, 6097 AllowRetry: false, 6098 RetryNeedsBackOff: false, 6099 } 6100 } 6101 } 6102 } else { 6103 // no error 6104 return successCount, nil 6105 } 6106 } 6107 6108 // CreateTable creates a new dynamodb table to the default aws region (as configured by aws cli) 6109 func (d *DynamoDB) CreateTable(input *dynamodb.CreateTableInput, ctx ...aws.Context) (*dynamodb.CreateTableOutput, error) { 6110 if d.cn == nil { 6111 return nil, fmt.Errorf("DynamoDB CreateTable Failed: " + "No DynamoDB Connection Available") 6112 } 6113 6114 if input == nil { 6115 return nil, fmt.Errorf("DynamoDB CreateTable Failed: " + "Input Object is Required") 6116 } 6117 6118 if len(ctx) <= 0 { 6119 return d.cn.CreateTable(input) 6120 } else { 6121 return d.cn.CreateTableWithContext(ctx[0], input) 6122 } 6123 } 6124 6125 // UpdateTable updates an existing dynamodb table with provided input parameter 6126 func (d *DynamoDB) UpdateTable(input *dynamodb.UpdateTableInput, ctx ...aws.Context) (*dynamodb.UpdateTableOutput, error) { 6127 if d.cn == nil { 6128 return nil, fmt.Errorf("DynamoDB UpdateTable Failed: " + "No DynamoDB Connection Available") 6129 } 6130 6131 if input == nil { 6132 return nil, fmt.Errorf("DynamoDB UpdateTable Failed: " + "Input Object is Required") 6133 } 6134 6135 if len(ctx) <= 0 { 6136 return d.cn.UpdateTable(input) 6137 } else { 6138 return d.cn.UpdateTableWithContext(ctx[0], input) 6139 } 6140 } 6141 6142 // DeleteTable deletes an existing dynamodb table 6143 func (d *DynamoDB) DeleteTable(input *dynamodb.DeleteTableInput, ctx ...aws.Context) (*dynamodb.DeleteTableOutput, error) { 6144 if d.cn == nil { 6145 return nil, fmt.Errorf("DynamoDB DeleteTable Failed: " + "No DynamoDB Connection Available") 6146 } 6147 6148 if input == nil { 6149 return nil, fmt.Errorf("DynamoDB DeleteTable Failed: " + "Input Object is Required") 6150 } 6151 6152 if len(ctx) <= 0 { 6153 return d.cn.DeleteTable(input) 6154 } else { 6155 return d.cn.DeleteTableWithContext(ctx[0], input) 6156 } 6157 } 6158 6159 // ListTables queries dynamodb tables list and returns found tables info 6160 func (d *DynamoDB) ListTables(input *dynamodb.ListTablesInput, ctx ...aws.Context) (*dynamodb.ListTablesOutput, error) { 6161 if d.cn == nil { 6162 return nil, fmt.Errorf("DynamoDB ListTables Failed: " + "No DynamoDB Connection Available") 6163 } 6164 6165 if input == nil { 6166 return nil, fmt.Errorf("DynamoDB ListTable Failed: " + "Input Object is Required") 6167 } 6168 6169 if len(ctx) <= 0 { 6170 return d.cn.ListTables(input) 6171 } else { 6172 return d.cn.ListTablesWithContext(ctx[0], input) 6173 } 6174 } 6175 6176 // DescribeTable describes the dynamodb table info for target identified in input parameter 6177 func (d *DynamoDB) DescribeTable(input *dynamodb.DescribeTableInput, ctx ...aws.Context) (*dynamodb.DescribeTableOutput, error) { 6178 if d.cn == nil { 6179 return nil, fmt.Errorf("DynamoDB DescribeTable Failed: " + "No DynamoDB Connection Available") 6180 } 6181 6182 if input == nil { 6183 return nil, fmt.Errorf("DynamoDB DescribeTable Failed: " + "Input Object is Required") 6184 } 6185 6186 if len(ctx) <= 0 { 6187 return d.cn.DescribeTable(input) 6188 } else { 6189 return d.cn.DescribeTableWithContext(ctx[0], input) 6190 } 6191 } 6192 6193 // CreateGlobalTable creates a dynamodb global table 6194 func (d *DynamoDB) CreateGlobalTable(input *dynamodb.CreateGlobalTableInput, ctx ...aws.Context) (*dynamodb.CreateGlobalTableOutput, error) { 6195 if d.cn == nil { 6196 return nil, fmt.Errorf("DynamoDB CreateGlobalTable Failed: " + "No DynamoDB Connection Available") 6197 } 6198 6199 if input == nil { 6200 return nil, fmt.Errorf("DynamoDB CreateGlobalTable Failed: " + "Input Object is Required") 6201 } 6202 6203 if len(ctx) <= 0 { 6204 return d.cn.CreateGlobalTable(input) 6205 } else { 6206 return d.cn.CreateGlobalTableWithContext(ctx[0], input) 6207 } 6208 } 6209 6210 // UpdateGlobalTable updates a dynamodb global table 6211 func (d *DynamoDB) UpdateGlobalTable(input *dynamodb.UpdateGlobalTableInput, ctx ...aws.Context) (*dynamodb.UpdateGlobalTableOutput, error) { 6212 if d.cn == nil { 6213 return nil, fmt.Errorf("DynamoDB UpdateGlobalTable Failed: " + "No DynamoDB Connection Available") 6214 } 6215 6216 if input == nil { 6217 return nil, fmt.Errorf("DynamoDB UpdateGlobalTable Failed: " + "Input Object is Required") 6218 } 6219 6220 if len(ctx) <= 0 { 6221 return d.cn.UpdateGlobalTable(input) 6222 } else { 6223 return d.cn.UpdateGlobalTableWithContext(ctx[0], input) 6224 } 6225 } 6226 6227 // ListGlobalTables lists dynamodb global tables 6228 func (d *DynamoDB) ListGlobalTables(input *dynamodb.ListGlobalTablesInput, ctx ...aws.Context) (*dynamodb.ListGlobalTablesOutput, error) { 6229 if d.cn == nil { 6230 return nil, fmt.Errorf("DynamoDB ListGlobalTables Failed: " + "No DynamoDB Connection Available") 6231 } 6232 6233 if input == nil { 6234 return nil, fmt.Errorf("DynamoDB ListGlobalTables Failed: " + "Input Object is Required") 6235 } 6236 6237 if len(ctx) <= 0 { 6238 return d.cn.ListGlobalTables(input) 6239 } else { 6240 return d.cn.ListGlobalTablesWithContext(ctx[0], input) 6241 } 6242 } 6243 6244 // DescribeGlobalTable describes dynamodb global table 6245 func (d *DynamoDB) DescribeGlobalTable(input *dynamodb.DescribeGlobalTableInput, ctx ...aws.Context) (*dynamodb.DescribeGlobalTableOutput, error) { 6246 if d.cn == nil { 6247 return nil, fmt.Errorf("DynamoDB DescribeGlobalTable Failed: " + "No DynamoDB Connection Available") 6248 } 6249 6250 if input == nil { 6251 return nil, fmt.Errorf("DynamoDB DescribeGlobalTable Failed: " + "Input Object is Required") 6252 } 6253 6254 if len(ctx) <= 0 { 6255 return d.cn.DescribeGlobalTable(input) 6256 } else { 6257 return d.cn.DescribeGlobalTableWithContext(ctx[0], input) 6258 } 6259 } 6260 6261 // CreateBackup creates dynamodb table backup 6262 func (d *DynamoDB) CreateBackup(input *dynamodb.CreateBackupInput, ctx ...aws.Context) (*dynamodb.CreateBackupOutput, error) { 6263 if d.cn == nil { 6264 return nil, fmt.Errorf("DynamoDB CreateBackup Failed: " + "No DynamoDB Connection Available") 6265 } 6266 6267 if input == nil { 6268 return nil, fmt.Errorf("DynamoDB CreateBackup Failed: " + "Input Object is Required") 6269 } 6270 6271 if len(ctx) <= 0 { 6272 return d.cn.CreateBackup(input) 6273 } else { 6274 return d.cn.CreateBackupWithContext(ctx[0], input) 6275 } 6276 } 6277 6278 // DeleteBackup deletes an existing dynamodb table backup 6279 func (d *DynamoDB) DeleteBackup(input *dynamodb.DeleteBackupInput, ctx ...aws.Context) (*dynamodb.DeleteBackupOutput, error) { 6280 if d.cn == nil { 6281 return nil, fmt.Errorf("DynamoDB DeleteBackup Failed: " + "No DynamoDB Connection Available") 6282 } 6283 6284 if input == nil { 6285 return nil, fmt.Errorf("DynamoDB DeleteBackup Failed: " + "Input Object is Required") 6286 } 6287 6288 if len(ctx) <= 0 { 6289 return d.cn.DeleteBackup(input) 6290 } else { 6291 return d.cn.DeleteBackupWithContext(ctx[0], input) 6292 } 6293 } 6294 6295 // ListBackups lists dynamodb table backup 6296 func (d *DynamoDB) ListBackups(input *dynamodb.ListBackupsInput, ctx ...aws.Context) (*dynamodb.ListBackupsOutput, error) { 6297 if d.cn == nil { 6298 return nil, fmt.Errorf("DynamoDB ListBackups Failed: " + "No DynamoDB Connection Available") 6299 } 6300 6301 if input == nil { 6302 return nil, fmt.Errorf("DynamoDB ListBackups Failed: " + "Input Object is Required") 6303 } 6304 6305 if len(ctx) <= 0 { 6306 return d.cn.ListBackups(input) 6307 } else { 6308 return d.cn.ListBackupsWithContext(ctx[0], input) 6309 } 6310 } 6311 6312 // DescribeBackup describes dynamodb table backup 6313 func (d *DynamoDB) DescribeBackup(input *dynamodb.DescribeBackupInput, ctx ...aws.Context) (*dynamodb.DescribeBackupOutput, error) { 6314 if d.cn == nil { 6315 return nil, fmt.Errorf("DynamoDB DescribeBackup Failed: " + "No DynamoDB Connection Available") 6316 } 6317 6318 if input == nil { 6319 return nil, fmt.Errorf("DynamoDB DescribeBackup Failed: " + "Input Object is Required") 6320 } 6321 6322 if len(ctx) <= 0 { 6323 return d.cn.DescribeBackup(input) 6324 } else { 6325 return d.cn.DescribeBackupWithContext(ctx[0], input) 6326 } 6327 } 6328 6329 // UpdatePointInTimeBackup updates dynamodb table point in time backup option 6330 func (d *DynamoDB) UpdatePointInTimeBackup(input *dynamodb.UpdateContinuousBackupsInput, ctx ...aws.Context) (*dynamodb.UpdateContinuousBackupsOutput, error) { 6331 if d.cn == nil { 6332 return nil, fmt.Errorf("DynamoDB UpdatePointInTimeBackup Failed: " + "No DynamoDB Connection Available") 6333 } 6334 6335 if input == nil { 6336 return nil, fmt.Errorf("DynamoDB UpdatePointInTimeBackup Failed: " + "Input Object is Required") 6337 } 6338 6339 if len(ctx) <= 0 { 6340 return d.cn.UpdateContinuousBackups(input) 6341 } else { 6342 return d.cn.UpdateContinuousBackupsWithContext(ctx[0], input) 6343 } 6344 }