1package expression 2 3import ( 4 "fmt" 5 "sort" 6 7 "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" 8) 9 10// expressionType specifies the type of Expression. Declaring this type is used 11// to eliminate magic strings 12type expressionType string 13 14const ( 15 projection expressionType = "projection" 16 keyCondition = "keyCondition" 17 condition = "condition" 18 filter = "filter" 19 update = "update" 20) 21 22// Implement the Sort interface 23type typeList []expressionType 24 25func (l typeList) Len() int { 26 return len(l) 27} 28 29func (l typeList) Less(i, j int) bool { 30 return string(l[i]) < string(l[j]) 31} 32 33func (l typeList) Swap(i, j int) { 34 l[i], l[j] = l[j], l[i] 35} 36 37// Builder represents the struct that builds the Expression struct. Methods such 38// as WithProjection() and WithCondition() can add different kinds of DynamoDB 39// Expressions to the Builder. The method Build() creates an Expression struct 40// with the specified types of DynamoDB Expressions. 41// 42// Example: 43// 44// keyCond := expression.Key("someKey").Equal(expression.Value("someValue")) 45// proj := expression.NamesList(expression.Name("aName"), expression.Name("anotherName"), expression.Name("oneOtherName")) 46// 47// builder := expression.NewBuilder().WithKeyCondition(keyCond).WithProjection(proj) 48// expr := builder.Build() 49// 50// queryInput := dynamodb.QueryInput{ 51// KeyConditionExpression: expr.KeyCondition(), 52// ProjectionExpression: expr.Projection(), 53// ExpressionAttributeNames: expr.Names(), 54// ExpressionAttributeValues: expr.Values(), 55// TableName: aws.String("SomeTable"), 56// } 57type Builder struct { 58 expressionMap map[expressionType]treeBuilder 59} 60 61// NewBuilder returns an empty Builder struct. Methods such as WithProjection() 62// and WithCondition() can add different kinds of DynamoDB Expressions to the 63// Builder. The method Build() creates an Expression struct with the specified 64// types of DynamoDB Expressions. 65// 66// Example: 67// 68// keyCond := expression.Key("someKey").Equal(expression.Value("someValue")) 69// proj := expression.NamesList(expression.Name("aName"), expression.Name("anotherName"), expression.Name("oneOtherName")) 70// builder := expression.NewBuilder().WithKeyCondition(keyCond).WithProjection(proj) 71func NewBuilder() Builder { 72 return Builder{} 73} 74 75// Build builds an Expression struct representing multiple types of DynamoDB 76// Expressions. Getter methods on the resulting Expression struct returns the 77// DynamoDB Expression strings as well as the maps that correspond to 78// ExpressionAttributeNames and ExpressionAttributeValues. Calling Build() on an 79// empty Builder returns the typed error EmptyParameterError. 80// 81// Example: 82// 83// // keyCond represents the Key Condition Expression 84// keyCond := expression.Key("someKey").Equal(expression.Value("someValue")) 85// // proj represents the Projection Expression 86// proj := expression.NamesList(expression.Name("aName"), expression.Name("anotherName"), expression.Name("oneOtherName")) 87// 88// // Add keyCond and proj to builder as a Key Condition and Projection 89// // respectively 90// builder := expression.NewBuilder().WithKeyCondition(keyCond).WithProjection(proj) 91// expr := builder.Build() 92// 93// queryInput := dynamodb.QueryInput{ 94// KeyConditionExpression: expr.KeyCondition(), 95// ProjectionExpression: expr.Projection(), 96// ExpressionAttributeNames: expr.Names(), 97// ExpressionAttributeValues: expr.Values(), 98// TableName: aws.String("SomeTable"), 99// } 100func (b Builder) Build() (Expression, error) { 101 if b.expressionMap == nil { 102 return Expression{}, newUnsetParameterError("Build", "Builder") 103 } 104 105 aliasList, expressionMap, err := b.buildChildTrees() 106 if err != nil { 107 return Expression{}, err 108 } 109 110 expression := Expression{ 111 expressionMap: expressionMap, 112 } 113 114 if len(aliasList.namesList) != 0 { 115 namesMap := map[string]string{} 116 for ind, val := range aliasList.namesList { 117 namesMap[fmt.Sprintf("#%v", ind)] = val 118 } 119 expression.namesMap = namesMap 120 } 121 122 if len(aliasList.valuesList) != 0 { 123 valuesMap := map[string]types.AttributeValue{} 124 for i := 0; i < len(aliasList.valuesList); i++ { 125 valuesMap[fmt.Sprintf(":%v", i)] = aliasList.valuesList[i] 126 } 127 expression.valuesMap = valuesMap 128 } 129 130 return expression, nil 131} 132 133// buildChildTrees compiles the list of treeBuilders that are the children of 134// the argument Builder. The returned aliasList represents all the alias tokens 135// used in the expression strings. The returned map[string]string maps the type 136// of expression (i.e. "condition", "update") to the appropriate expression 137// string. 138func (b Builder) buildChildTrees() (aliasList, map[expressionType]string, error) { 139 aList := aliasList{} 140 formattedExpressions := map[expressionType]string{} 141 keys := typeList{} 142 143 for expressionType := range b.expressionMap { 144 keys = append(keys, expressionType) 145 } 146 147 sort.Sort(keys) 148 149 for _, key := range keys { 150 node, err := b.expressionMap[key].buildTree() 151 if err != nil { 152 return aliasList{}, nil, err 153 } 154 formattedExpression, err := node.buildExpressionString(&aList) 155 if err != nil { 156 return aliasList{}, nil, err 157 } 158 formattedExpressions[key] = formattedExpression 159 } 160 161 return aList, formattedExpressions, nil 162} 163 164// WithCondition method adds the argument ConditionBuilder as a Condition 165// Expression to the argument Builder. If the argument Builder already has a 166// ConditionBuilder representing a Condition Expression, WithCondition() 167// overwrites the existing ConditionBuilder. 168// 169// Example: 170// 171// // let builder be an existing Builder{} and cond be an existing 172// // ConditionBuilder{} 173// builder = builder.WithCondition(cond) 174// 175// // add other DynamoDB Expressions to the builder. let proj be an already 176// // existing ProjectionBuilder 177// builder = builder.WithProjection(proj) 178// // create an Expression struct 179// expr := builder.Build() 180func (b Builder) WithCondition(conditionBuilder ConditionBuilder) Builder { 181 if b.expressionMap == nil { 182 b.expressionMap = map[expressionType]treeBuilder{} 183 } 184 b.expressionMap[condition] = conditionBuilder 185 return b 186} 187 188// WithProjection method adds the argument ProjectionBuilder as a Projection 189// Expression to the argument Builder. If the argument Builder already has a 190// ProjectionBuilder representing a Projection Expression, WithProjection() 191// overwrites the existing ProjectionBuilder. 192// 193// Example: 194// 195// // let builder be an existing Builder{} and proj be an existing 196// // ProjectionBuilder{} 197// builder = builder.WithProjection(proj) 198// 199// // add other DynamoDB Expressions to the builder. let cond be an already 200// // existing ConditionBuilder 201// builder = builder.WithCondition(cond) 202// // create an Expression struct 203// expr := builder.Build() 204func (b Builder) WithProjection(projectionBuilder ProjectionBuilder) Builder { 205 if b.expressionMap == nil { 206 b.expressionMap = map[expressionType]treeBuilder{} 207 } 208 b.expressionMap[projection] = projectionBuilder 209 return b 210} 211 212// WithKeyCondition method adds the argument KeyConditionBuilder as a Key 213// Condition Expression to the argument Builder. If the argument Builder already 214// has a KeyConditionBuilder representing a Key Condition Expression, 215// WithKeyCondition() overwrites the existing KeyConditionBuilder. 216// 217// Example: 218// 219// // let builder be an existing Builder{} and keyCond be an existing 220// // KeyConditionBuilder{} 221// builder = builder.WithKeyCondition(keyCond) 222// 223// // add other DynamoDB Expressions to the builder. let cond be an already 224// // existing ConditionBuilder 225// builder = builder.WithCondition(cond) 226// // create an Expression struct 227// expr := builder.Build() 228func (b Builder) WithKeyCondition(keyConditionBuilder KeyConditionBuilder) Builder { 229 if b.expressionMap == nil { 230 b.expressionMap = map[expressionType]treeBuilder{} 231 } 232 b.expressionMap[keyCondition] = keyConditionBuilder 233 return b 234} 235 236// WithFilter method adds the argument ConditionBuilder as a Filter Expression 237// to the argument Builder. If the argument Builder already has a 238// ConditionBuilder representing a Filter Expression, WithFilter() 239// overwrites the existing ConditionBuilder. 240// 241// Example: 242// 243// // let builder be an existing Builder{} and filt be an existing 244// // ConditionBuilder{} 245// builder = builder.WithFilter(filt) 246// 247// // add other DynamoDB Expressions to the builder. let cond be an already 248// // existing ConditionBuilder 249// builder = builder.WithCondition(cond) 250// // create an Expression struct 251// expr := builder.Build() 252func (b Builder) WithFilter(filterBuilder ConditionBuilder) Builder { 253 if b.expressionMap == nil { 254 b.expressionMap = map[expressionType]treeBuilder{} 255 } 256 b.expressionMap[filter] = filterBuilder 257 return b 258} 259 260// WithUpdate method adds the argument UpdateBuilder as an Update Expression 261// to the argument Builder. If the argument Builder already has a UpdateBuilder 262// representing a Update Expression, WithUpdate() overwrites the existing 263// UpdateBuilder. 264// 265// Example: 266// 267// // let builder be an existing Builder{} and update be an existing 268// // UpdateBuilder{} 269// builder = builder.WithUpdate(update) 270// 271// // add other DynamoDB Expressions to the builder. let cond be an already 272// // existing ConditionBuilder 273// builder = builder.WithCondition(cond) 274// // create an Expression struct 275// expr := builder.Build() 276func (b Builder) WithUpdate(updateBuilder UpdateBuilder) Builder { 277 if b.expressionMap == nil { 278 b.expressionMap = map[expressionType]treeBuilder{} 279 } 280 b.expressionMap[update] = updateBuilder 281 return b 282} 283 284// Expression represents a collection of DynamoDB Expressions. The getter 285// methods of the Expression struct retrieves the formatted DynamoDB 286// Expressions, ExpressionAttributeNames, and ExpressionAttributeValues. 287// 288// Example: 289// 290// // keyCond represents the Key Condition Expression 291// keyCond := expression.Key("someKey").Equal(expression.Value("someValue")) 292// // proj represents the Projection Expression 293// proj := expression.NamesList(expression.Name("aName"), expression.Name("anotherName"), expression.Name("oneOtherName")) 294// 295// // Add keyCond and proj to builder as a Key Condition and Projection 296// // respectively 297// builder := expression.NewBuilder().WithKeyCondition(keyCond).WithProjection(proj) 298// expr := builder.Build() 299// 300// queryInput := dynamodb.QueryInput{ 301// KeyConditionExpression: expr.KeyCondition(), 302// ProjectionExpression: expr.Projection(), 303// ExpressionAttributeNames: expr.Names(), 304// ExpressionAttributeValues: expr.Values(), 305// TableName: aws.String("SomeTable"), 306// } 307type Expression struct { 308 expressionMap map[expressionType]string 309 namesMap map[string]string 310 valuesMap map[string]types.AttributeValue 311} 312 313// treeBuilder interface is fulfilled by builder structs that represent 314// different types of Expressions. 315type treeBuilder interface { 316 // buildTree creates the tree structure of exprNodes. The tree structure 317 // of exprNodes are traversed in order to build the string representing 318 // different types of Expressions as well as the maps that represent 319 // ExpressionAttributeNames and ExpressionAttributeValues. 320 buildTree() (exprNode, error) 321} 322 323// Condition returns the *string corresponding to the Condition Expression 324// of the argument Expression. This method is used to satisfy the members of 325// DynamoDB input structs. If the Expression does not have a condition 326// expression this method returns nil. 327// 328// Example: 329// 330// // let expression be an instance of Expression{} 331// 332// deleteInput := dynamodb.DeleteItemInput{ 333// ConditionExpression: expression.Condition(), 334// ExpressionAttributeNames: expression.Names(), 335// ExpressionAttributeValues: expression.Values(), 336// Key: map[string]types.AttributeValue{ 337// "PartitionKey": &types.AttributeValueMemberS{Value: "SomeKey"}, 338// }, 339// TableName: aws.String("SomeTable"), 340// } 341func (e Expression) Condition() *string { 342 return e.returnExpression(condition) 343} 344 345// Filter returns the *string corresponding to the Filter Expression of the 346// argument Expression. This method is used to satisfy the members of DynamoDB 347// input structs. If the Expression does not have a filter expression this 348// method returns nil. 349// 350// Example: 351// 352// // let expression be an instance of Expression{} 353// 354// queryInput := dynamodb.QueryInput{ 355// KeyConditionExpression: expression.KeyCondition(), 356// FilterExpression: expression.Filter(), 357// ExpressionAttributeNames: expression.Names(), 358// ExpressionAttributeValues: expression.Values(), 359// TableName: aws.String("SomeTable"), 360// } 361func (e Expression) Filter() *string { 362 return e.returnExpression(filter) 363} 364 365// Projection returns the *string corresponding to the Projection Expression 366// of the argument Expression. This method is used to satisfy the members of 367// DynamoDB input structs. If the Expression does not have a projection 368// expression this method returns nil. 369// 370// Example: 371// 372// // let expression be an instance of Expression{} 373// 374// queryInput := dynamodb.QueryInput{ 375// KeyConditionExpression: expression.KeyCondition(), 376// ProjectionExpression: expression.Projection(), 377// ExpressionAttributeNames: expression.Names(), 378// ExpressionAttributeValues: expression.Values(), 379// TableName: aws.String("SomeTable"), 380// } 381func (e Expression) Projection() *string { 382 return e.returnExpression(projection) 383} 384 385// KeyCondition returns the *string corresponding to the Key Condition 386// Expression of the argument Expression. This method is used to satisfy the 387// members of DynamoDB input structs. If the argument Expression does not have a 388// KeyConditionExpression, KeyCondition() returns nil. 389// 390// Example: 391// 392// // let expression be an instance of Expression{} 393// 394// queryInput := dynamodb.QueryInput{ 395// KeyConditionExpression: expression.KeyCondition(), 396// ProjectionExpression: expression.Projection(), 397// ExpressionAttributeNames: expression.Names(), 398// ExpressionAttributeValues: expression.Values(), 399// TableName: aws.String("SomeTable"), 400// } 401func (e Expression) KeyCondition() *string { 402 return e.returnExpression(keyCondition) 403} 404 405// Update returns the *string corresponding to the Update Expression of the 406// argument Expression. This method is used to satisfy the members of DynamoDB 407// input structs. If the argument Expression does not have a UpdateExpression, 408// Update() returns nil. 409// 410// Example: 411// 412// // let expression be an instance of Expression{} 413// 414// updateInput := dynamodb.UpdateInput{ 415// Key: map[string]types.AttributeValue{ 416// "PartitionKey": &types.AttributeValueMemberS{Value: "someKey"}, 417// }, 418// UpdateExpression: expression.Update(), 419// ExpressionAttributeNames: expression.Names(), 420// ExpressionAttributeValues: expression.Values(), 421// TableName: aws.String("SomeTable"), 422// } 423func (e Expression) Update() *string { 424 return e.returnExpression(update) 425} 426 427// Names returns the map[string]*string corresponding to the 428// ExpressionAttributeNames of the argument Expression. This method is used to 429// satisfy the members of DynamoDB input structs. If Expression does not use 430// ExpressionAttributeNames, this method returns nil. The 431// ExpressionAttributeNames and ExpressionAttributeValues member of the input 432// struct must always be assigned when using the Expression struct since all 433// item attribute names and values are aliased. That means that if the 434// ExpressionAttributeNames and ExpressionAttributeValues member is not assigned 435// with the corresponding Names() and Values() methods, the DynamoDB operation 436// will run into a logic error. 437// 438// Example: 439// 440// // let expression be an instance of Expression{} 441// 442// queryInput := dynamodb.QueryInput{ 443// KeyConditionExpression: expression.KeyCondition(), 444// ProjectionExpression: expression.Projection(), 445// ExpressionAttributeNames: expression.Names(), 446// ExpressionAttributeValues: expression.Values(), 447// TableName: aws.String("SomeTable"), 448// } 449func (e Expression) Names() map[string]string { 450 return e.namesMap 451} 452 453// Values returns the map[string]*dynamodb.AttributeValue corresponding to 454// the ExpressionAttributeValues of the argument Expression. This method is used 455// to satisfy the members of DynamoDB input structs. If Expression does not use 456// ExpressionAttributeValues, this method returns nil. The 457// ExpressionAttributeNames and ExpressionAttributeValues member of the input 458// struct must always be assigned when using the Expression struct since all 459// item attribute names and values are aliased. That means that if the 460// ExpressionAttributeNames and ExpressionAttributeValues member is not assigned 461// with the corresponding Names() and Values() methods, the DynamoDB operation 462// will run into a logic error. 463// 464// Example: 465// 466// // let expression be an instance of Expression{} 467// 468// queryInput := dynamodb.QueryInput{ 469// KeyConditionExpression: expression.KeyCondition(), 470// ProjectionExpression: expression.Projection(), 471// ExpressionAttributeNames: expression.Names(), 472// ExpressionAttributeValues: expression.Values(), 473// TableName: aws.String("SomeTable"), 474// } 475func (e Expression) Values() map[string]types.AttributeValue { 476 return e.valuesMap 477} 478 479// returnExpression returns *string corresponding to the type of Expression 480// string specified by the expressionType. If there is no corresponding 481// expression available in Expression, the method returns nil 482func (e Expression) returnExpression(expressionType expressionType) *string { 483 if e.expressionMap == nil { 484 return nil 485 } 486 if s, exists := e.expressionMap[expressionType]; exists { 487 return &s 488 } 489 return nil 490} 491 492// exprNode are the generic nodes that represents both Operands and 493// Conditions. The purpose of exprNode is to be able to call an generic 494// recursive function on the top level exprNode to be able to determine a root 495// node in order to deduplicate name aliases. 496// fmtExpr is a string that has escaped characters to refer to 497// names/values/children which needs to be aliased at runtime in order to avoid 498// duplicate values. The rules are as follows: 499// $n: Indicates that an alias of a name needs to be inserted. The 500// corresponding name to be alias is in the []names slice. 501// $v: Indicates that an alias of a value needs to be inserted. The 502// corresponding value to be alias is in the []values slice. 503// $c: Indicates that the fmtExpr of a child exprNode needs to be inserted. 504// The corresponding child node is in the []children slice. 505type exprNode struct { 506 names []string 507 values []types.AttributeValue 508 children []exprNode 509 fmtExpr string 510} 511 512// aliasList keeps track of all the names we need to alias in the nested 513// struct of conditions and operands. This allows each alias to be unique. 514// aliasList is passed in as a pointer when buildChildTrees is called in 515// order to deduplicate all names within the tree strcuture of the exprNodes. 516type aliasList struct { 517 namesList []string 518 valuesList []types.AttributeValue 519} 520 521// buildExpressionString returns a string with aliasing for names/values 522// specified by aliasList. The string corresponds to the expression that the 523// exprNode tree represents. 524func (en exprNode) buildExpressionString(aliasList *aliasList) (string, error) { 525 // Since each exprNode contains a slice of names, values, and children that 526 // correspond to the escaped characters, we an index to traverse the slices 527 index := struct { 528 name, value, children int 529 }{} 530 531 formattedExpression := en.fmtExpr 532 533 for i := 0; i < len(formattedExpression); { 534 if formattedExpression[i] != '$' { 535 i++ 536 continue 537 } 538 539 if i == len(formattedExpression)-1 { 540 return "", fmt.Errorf("buildexprNode error: invalid escape character") 541 } 542 543 var alias string 544 var err error 545 // if an escaped character is found, substitute it with the proper alias 546 // TODO consider AST instead of string in the future 547 switch formattedExpression[i+1] { 548 case 'n': 549 alias, err = substitutePath(index.name, en, aliasList) 550 if err != nil { 551 return "", err 552 } 553 index.name++ 554 555 case 'v': 556 alias, err = substituteValue(index.value, en, aliasList) 557 if err != nil { 558 return "", err 559 } 560 index.value++ 561 562 case 'c': 563 alias, err = substituteChild(index.children, en, aliasList) 564 if err != nil { 565 return "", err 566 } 567 index.children++ 568 569 default: 570 return "", fmt.Errorf("buildexprNode error: invalid escape rune %#v", formattedExpression[i+1]) 571 } 572 formattedExpression = formattedExpression[:i] + alias + formattedExpression[i+2:] 573 i += len(alias) 574 } 575 576 return formattedExpression, nil 577} 578 579// substitutePath substitutes the escaped character $n with the appropriate 580// alias. 581func substitutePath(index int, node exprNode, aliasList *aliasList) (string, error) { 582 if index >= len(node.names) { 583 return "", fmt.Errorf("substitutePath error: exprNode []names out of range") 584 } 585 str, err := aliasList.aliasPath(node.names[index]) 586 if err != nil { 587 return "", err 588 } 589 return str, nil 590} 591 592// substituteValue substitutes the escaped character $v with the appropriate 593// alias. 594func substituteValue(index int, node exprNode, aliasList *aliasList) (string, error) { 595 if index >= len(node.values) { 596 return "", fmt.Errorf("substituteValue error: exprNode []values out of range") 597 } 598 str, err := aliasList.aliasValue(node.values[index]) 599 if err != nil { 600 return "", err 601 } 602 return str, nil 603} 604 605// substituteChild substitutes the escaped character $c with the appropriate 606// alias. 607func substituteChild(index int, node exprNode, aliasList *aliasList) (string, error) { 608 if index >= len(node.children) { 609 return "", fmt.Errorf("substituteChild error: exprNode []children out of range") 610 } 611 return node.children[index].buildExpressionString(aliasList) 612} 613 614// aliasValue returns the corresponding alias to the dav value argument. Since 615// values are not deduplicated as of now, all values are just appended to the 616// aliasList and given the index as the alias. 617func (al *aliasList) aliasValue(dav types.AttributeValue) (string, error) { 618 al.valuesList = append(al.valuesList, dav) 619 return fmt.Sprintf(":%d", len(al.valuesList)-1), nil 620} 621 622// aliasPath returns the corresponding alias to the argument string. The 623// argument is checked against all existing aliasList names in order to avoid 624// duplicate strings getting two different aliases. 625func (al *aliasList) aliasPath(nm string) (string, error) { 626 for ind, name := range al.namesList { 627 if nm == name { 628 return fmt.Sprintf("#%d", ind), nil 629 } 630 } 631 al.namesList = append(al.namesList, nm) 632 return fmt.Sprintf("#%d", len(al.namesList)-1), nil 633} 634