github.com/haagen/force@v0.19.6-0.20140911230915-22addd930b34/display.go (about) 1 package main 2 3 import ( 4 "bytes" 5 "fmt" 6 "sort" 7 "strings" 8 ) 9 10 var BatchInfoTemplate = ` 11 Id %s 12 JobId %s 13 State %s 14 CreatedDate %s 15 SystemModstamp %s 16 NumberRecordsProcessed %d 17 ` 18 19 func DisplayBatchList(batchInfos []BatchInfo) { 20 21 for i, batchInfo := range batchInfos { 22 fmt.Printf("Batch %d", i) 23 DisplayBatchInfo(batchInfo) 24 fmt.Println() 25 } 26 } 27 28 func DisplayBatchInfo(batchInfo BatchInfo) { 29 30 fmt.Printf(BatchInfoTemplate, batchInfo.Id, batchInfo.JobId, batchInfo.State, 31 batchInfo.CreatedDate, batchInfo.SystemModstamp, 32 batchInfo.NumberRecordsProcessed) 33 } 34 35 func DisplayJobInfo(jobInfo JobInfo) { 36 var msg = ` 37 Id %s 38 State %s 39 Operation %s 40 Object %s 41 Api Version %s 42 43 Created By Id %s 44 Created Date %s 45 System Mod Stamp %s 46 Content Type %s 47 Concurrency Mode %s 48 49 Number Batches Queued %d 50 Number Batches In Progress %d 51 Number Batches Completed %d 52 Number Batches Failed %d 53 Number Batches Total %d 54 Number Records Processed %d 55 Number Retries %d 56 57 Number Records Failed %d 58 Total Processing Time %d 59 Api Active Processing Time %d 60 Apex Processing Time %d 61 ` 62 fmt.Printf(msg, jobInfo.Id, jobInfo.State, jobInfo.Operation, jobInfo.Object, jobInfo.ApiVersion, 63 jobInfo.CreatedById, jobInfo.CreatedDate, jobInfo.SystemModStamp, 64 jobInfo.ContentType, jobInfo.ConcurrencyMode, 65 jobInfo.NumberBatchesQueued, jobInfo.NumberBatchesInProgress, 66 jobInfo.NumberBatchesCompleted, jobInfo.NumberBatchesFailed, 67 jobInfo.NumberBatchesTotal, jobInfo.NumberRecordsProcessed, 68 jobInfo.NumberRetries, 69 jobInfo.NumberRecordsFailed, jobInfo.TotalProcessingTime, 70 jobInfo.ApiActiveProcessingTime, jobInfo.ApexProcessingTime) 71 } 72 73 func DisplayForceSobjects(sobjects []ForceSobject) { 74 names := make([]string, len(sobjects)) 75 for i, sobject := range sobjects { 76 names[i] = sobject["name"].(string) 77 } 78 sort.Strings(names) 79 for _, name := range names { 80 fmt.Println(name) 81 } 82 } 83 84 func DisplayForceRecordsf(records []ForceRecord, format string) { 85 switch format { 86 case "csv": 87 fmt.Println(RenderForceRecordsCSV(records, format)) 88 default: 89 fmt.Printf("Format %s not supported\n\n", format) 90 } 91 } 92 93 func DisplayForceRecords(result ForceQueryResult) { 94 if len(result.Records) > 0 { 95 fmt.Print(RenderForceRecords(result.Records)) 96 } 97 fmt.Println(fmt.Sprintf(" (%d records)", result.TotalSize)) 98 } 99 100 func recordColumns(records []ForceRecord) (columns []string) { 101 for _, record := range records { 102 for key, _ := range record { 103 found := false 104 for _, column := range columns { 105 if column == key { 106 found = true 107 break 108 } 109 } 110 if !found { 111 columns = append(columns, key) 112 } 113 } 114 } 115 return 116 } 117 118 func coerceForceRecords(uncoerced []map[string]interface{}) (records []ForceRecord) { 119 records = make([]ForceRecord, len(uncoerced)) 120 for i, record := range uncoerced { 121 records[i] = ForceRecord(record) 122 } 123 return 124 } 125 126 func columnLengths(records []ForceRecord, prefix string) (lengths map[string]int) { 127 lengths = make(map[string]int) 128 129 columns := recordColumns(records) 130 for _, column := range columns { 131 lengths[fmt.Sprintf("%s.%s", prefix, column)] = len(column) + 2 132 } 133 134 for _, record := range records { 135 for column, value := range record { 136 key := fmt.Sprintf("%s.%s", prefix, column) 137 length := 0 138 switch value := value.(type) { 139 case []ForceRecord: 140 lens := columnLengths(value, key) 141 for k, l := range lens { 142 length += l 143 if l > lengths[k] { 144 lengths[k] = l 145 } 146 } 147 length += len(lens) - 1 148 default: 149 if value == nil { 150 length = len(" (null) ") 151 } else { 152 length = len(fmt.Sprintf(" %v ", value)) 153 } 154 } 155 if length > lengths[key] { 156 lengths[key] = length 157 } 158 } 159 } 160 return 161 } 162 163 func recordHeader(columns []string, lengths map[string]int, prefix string) (out string) { 164 headers := make([]string, len(columns)) 165 for i, column := range columns { 166 key := fmt.Sprintf("%s.%s", prefix, column) 167 headers[i] = fmt.Sprintf(fmt.Sprintf(" %%-%ds ", lengths[key]-2), column) 168 } 169 out = strings.Join(headers, "|") 170 return 171 } 172 173 func recordSeparator(columns []string, lengths map[string]int, prefix string) (out string) { 174 separators := make([]string, len(columns)) 175 for i, column := range columns { 176 key := fmt.Sprintf("%s.%s", prefix, column) 177 separators[i] = strings.Repeat("-", lengths[key]) 178 } 179 out = strings.Join(separators, "+") 180 return 181 } 182 183 func recordRow(record ForceRecord, columns []string, lengths map[string]int, prefix string) (out string) { 184 values := make([]string, len(columns)) 185 for i, column := range columns { 186 value := record[column] 187 switch value := value.(type) { 188 case []ForceRecord: 189 values[i] = strings.TrimSuffix(renderForceRecords(value, fmt.Sprintf("%s.%s", prefix, column), lengths), "\n") 190 default: 191 if value == nil { 192 values[i] = fmt.Sprintf(fmt.Sprintf(" %%-%ds ", lengths[column]-2), "(null)") 193 } else { 194 values[i] = fmt.Sprintf(fmt.Sprintf(" %%-%dv ", lengths[column]-2), value) 195 } 196 } 197 } 198 maxrows := 1 199 for _, value := range values { 200 rows := len(strings.Split(value, "\n")) 201 if rows > maxrows { 202 maxrows = rows 203 } 204 } 205 rows := make([]string, maxrows) 206 for i := 0; i < maxrows; i++ { 207 rowvalues := make([]string, len(columns)) 208 for j, column := range columns { 209 key := fmt.Sprintf("%s.%s", prefix, column) 210 parts := strings.Split(values[j], "\n") 211 if i < len(parts) { 212 rowvalues[j] = fmt.Sprintf(fmt.Sprintf("%%-%ds", lengths[key]), parts[i]) 213 } else { 214 rowvalues[j] = strings.Repeat(" ", lengths[key]) 215 } 216 } 217 rows[i] = strings.Join(rowvalues, "|") 218 } 219 out = strings.Join(rows, "\n") 220 return 221 } 222 223 // returns first index of a given string 224 func StringSlicePos(slice []string, value string) int { 225 for p, v := range slice { 226 if v == value { 227 return p 228 } 229 } 230 return -1 231 } 232 233 // returns true if a slice contains given string 234 func StringSliceContains(slice []string, value string) bool { 235 return StringSlicePos(slice, value) > -1 236 } 237 238 func RenderForceRecordsCSV(records []ForceRecord, format string) string { 239 var out bytes.Buffer 240 241 var keys []string 242 var flattenedRecords []map[string]interface{} 243 for _, record := range records { 244 flattenedRecord := flattenForceRecord(record) 245 flattenedRecords = append(flattenedRecords, flattenedRecord) 246 for key, _ := range flattenedRecord { 247 if !StringSliceContains(keys, key) { 248 keys = append(keys, key) 249 } 250 } 251 } 252 //keys = RemoveTransientRelationships(keys) 253 f, _ := ActiveCredentials() 254 if len(records) > 0 { 255 lengths := make([]int, len(keys)) 256 outKeys := make([]string, len(keys)) 257 for i, key := range keys { 258 lengths[i] = len(key) 259 if strings.HasSuffix(key, "__c") && f.Namespace != "" { 260 outKeys[i] = fmt.Sprintf(`%%%`, f.Namespace, "__", key) 261 } else { 262 outKeys[i] = key 263 } 264 } 265 266 formatter_parts := make([]string, len(outKeys)) 267 for i, length := range lengths { 268 formatter_parts[i] = fmt.Sprintf(`"%%-%ds"`, length) 269 } 270 271 formatter := strings.Join(formatter_parts, `,`) 272 out.WriteString(fmt.Sprintf(formatter+"\n", StringSliceToInterfaceSlice(outKeys)...)) 273 for _, record := range flattenedRecords { 274 values := make([][]string, len(keys)) 275 for i, key := range keys { 276 values[i] = strings.Split(fmt.Sprintf(`%v`, record[key]), `\n`) 277 } 278 279 maxLines := 0 280 for _, value := range values { 281 lines := len(value) 282 if lines > maxLines { 283 maxLines = lines 284 } 285 } 286 287 for li := 0; li < maxLines; li++ { 288 line := make([]string, len(values)) 289 for i, value := range values { 290 if len(value) > li { 291 line[i] = strings.Replace(value[li], `"`, `'`, -1) 292 } 293 } 294 out.WriteString(fmt.Sprintf(formatter+"\n", StringSliceToInterfaceSlice(line)...)) 295 } 296 } 297 } 298 return out.String() 299 return "" 300 } 301 302 func flattenForceRecord(record ForceRecord) (flattened ForceRecord) { 303 flattened = make(ForceRecord) 304 for key, value := range record { 305 if key == "attributes" { 306 continue 307 } 308 switch value := value.(type) { 309 case map[string]interface{}: 310 if value["records"] != nil { 311 unflattened := value["records"].([]interface{}) 312 subflattened := make([]ForceRecord, len(unflattened)) 313 for i, record := range unflattened { 314 subflattened[i] = (map[string]interface{})(flattenForceRecord(ForceRecord(record.(map[string]interface{})))) 315 } 316 flattened[key] = subflattened 317 } else { 318 for k, v := range flattenForceRecord(value) { 319 flattened[fmt.Sprintf("%s.%s", key, k)] = v 320 } 321 } 322 default: 323 flattened[key] = value 324 } 325 } 326 return 327 } 328 329 func recordsHaveSubRows(records []ForceRecord) bool { 330 for _, record := range records { 331 for _, value := range record { 332 switch value := value.(type) { 333 case []ForceRecord: 334 if len(value) > 0 { 335 return true 336 } 337 } 338 } 339 } 340 return false 341 } 342 343 func renderForceRecords(records []ForceRecord, prefix string, lengths map[string]int) string { 344 var out bytes.Buffer 345 346 columns := recordColumns(records) 347 348 out.WriteString(recordHeader(columns, lengths, prefix) + "\n") 349 out.WriteString(recordSeparator(columns, lengths, prefix) + "\n") 350 351 for _, record := range records { 352 out.WriteString(recordRow(record, columns, lengths, prefix) + "\n") 353 if recordsHaveSubRows(records) { 354 out.WriteString(recordSeparator(columns, lengths, prefix) + "\n") 355 } 356 } 357 358 return out.String() 359 } 360 361 func RenderForceRecords(records []ForceRecord) string { 362 flattened := make([]ForceRecord, len(records)) 363 for i, record := range records { 364 flattened[i] = flattenForceRecord(record) 365 } 366 lengths := columnLengths(flattened, "") 367 return renderForceRecords(flattened, "", lengths) 368 } 369 370 func DisplayForceRecord(record ForceRecord) { 371 DisplayInterfaceMap(record, 0) 372 } 373 374 func DisplayInterfaceMap(object map[string]interface{}, indent int) { 375 keys := make([]string, len(object)) 376 i := 0 377 for key, _ := range object { 378 keys[i] = key 379 i++ 380 } 381 sort.Strings(keys) 382 for _, key := range keys { 383 for i := 0; i < indent; i++ { 384 fmt.Printf(" ") 385 } 386 fmt.Printf("%s: ", key) 387 switch v := object[key].(type) { 388 case map[string]interface{}: 389 fmt.Printf("\n") 390 DisplayInterfaceMap(v, indent+1) 391 default: 392 fmt.Printf("%v\n", v) 393 } 394 } 395 } 396 397 func StringSliceToInterfaceSlice(s []string) (i []interface{}) { 398 for _, str := range s { 399 i = append(i, interface{}(str)) 400 } 401 return 402 } 403 404 type ForceSobjectFields []interface{} 405 406 func DisplayForceSobject(sobject ForceSobject) { 407 fields := ForceSobjectFields(sobject["fields"].([]interface{})) 408 sort.Sort(fields) 409 for _, f := range fields { 410 field := f.(map[string]interface{}) 411 switch field["type"] { 412 case "picklist": 413 var values []string 414 for _, value := range field["picklistValues"].([]interface{}) { 415 values = append(values, value.(map[string]interface{})["value"].(string)) 416 } 417 fmt.Printf("%s: %s (%s)\n", field["name"], field["type"], strings.Join(values, ", ")) 418 case "reference": 419 var refs []string 420 for _, ref := range field["referenceTo"].([]interface{}) { 421 refs = append(refs, ref.(string)) 422 } 423 fmt.Printf("%s: %s (%s)\n", field["name"], field["type"], strings.Join(refs, ", ")) 424 default: 425 fmt.Printf("%s: %s\n", field["name"], field["type"]) 426 } 427 } 428 } 429 430 func DisplayFieldTypes() { 431 var msg = ` 432 text/string (length = 255) 433 textarea (length = 255) 434 longtextarea (length = 32768, visibleLines = 5) 435 richtextarea (length = 32768, visibleLines = 5) 436 checkbox/bool/boolean (defaultValue = false) 437 datetime () 438 float/double/currency (length = 16, precision = 2) 439 number/int (length = 18, precision = 0) 440 autonumber (displayFormat = "AN {00000}", startingNumber = 0) 441 geolocation (displayLocationInDecimal = true, scale = 5) 442 lookup (will be prompted for Object and label) 443 masterdetail (will be prompted for Object and label) 444 445 *To create a formula field add a formula argument to the command. 446 force field create <objectname> <fieldName>:text formula:'LOWER("HEY MAN")' 447 ` 448 fmt.Println(msg) 449 } 450 451 func DisplayFieldDetails(fieldType string) { 452 var msg = `` 453 switch fieldType { 454 case "text", "string": 455 msg = DisplayTextFieldDetails() 456 break 457 case "textarea": 458 msg = DisplayTextAreaFieldDetails() 459 break 460 case "longtextarea": 461 msg = DisplayLongTextAreaFieldDetails() 462 break 463 case "richtextarea": 464 msg = DisplayRichTextAreaFieldDetails() 465 break 466 case "checkbox", "bool", "boolean": 467 msg = DisplayCheckboxFieldDetails() 468 break 469 case "datetime": 470 msg = DisplayDatetimeFieldDetails() 471 break 472 case "float", "double", "currency": 473 if fieldType == "currency" { 474 msg = DisplayCurrencyFieldDetails() 475 } else { 476 msg = DisplayDoubleFieldDetails() 477 } 478 break 479 case "number", "int": 480 msg = DisplayDoubleFieldDetails() 481 break 482 case "autonumber": 483 msg = DisplayAutonumberFieldDetails() 484 break 485 case "geolocation": 486 msg = DisplayGeolocationFieldDetails() 487 break 488 case "lookup": 489 msg = DisplayLookupFieldDetails() 490 break 491 case "masterdetail": 492 msg = DisplayMasterDetailFieldDetails() 493 break 494 default: 495 msg = ` 496 Sorry, that is not a valid field type. 497 ` 498 } 499 fmt.Printf(msg + "\n") 500 } 501 502 func DisplayTextFieldDetails() (message string) { 503 return fmt.Sprintf(` 504 Allows users to enter any combination of letters and numbers. 505 506 %s 507 label - defaults to name 508 length - defaults to 255 509 name 510 511 %s 512 description 513 helptext 514 required - defaults to false 515 unique - defaults to false 516 caseSensistive - defaults to false 517 externalId - defaults to false 518 defaultValue 519 formula - defaultValue must be blask 520 formulaTreatBlanksAs - defaults to "BlankAsZero" 521 `, "\x1b[31;1mrequired attributes\x1b[0m", "\x1b[31;1moptional attributes\x1b[0m") 522 } 523 func DisplayTextAreaFieldDetails() (message string) { 524 return fmt.Sprintf(` 525 Allows users to enter up to 255 characters on separate lines. 526 527 %s 528 label - defaults to name 529 name 530 531 %s 532 description 533 helptext 534 required - defaults to false 535 defaultValue 536 `, "\x1b[31;1mrequired attributes\x1b[0m", "\x1b[31;1moptional attributes\x1b[0m") 537 } 538 func DisplayLongTextAreaFieldDetails() (message string) { 539 return fmt.Sprintf(` 540 Allows users to enter up to 32,768 characters on separate lines. 541 542 %s 543 label - defaults to name 544 length - defaults to 32,768 545 name 546 visibleLines - defaults to 3 547 548 %s 549 description 550 helptext 551 defaultValue 552 `, "\x1b[31;1mrequired attributes\x1b[0m", "\x1b[31;1moptional attributes\x1b[0m") 553 } 554 func DisplayRichTextAreaFieldDetails() (message string) { 555 return fmt.Sprintf(` 556 Allows users to enter formatted text, add images and links. Up to 32,768 characters on separate lines. 557 558 %s 559 label - defaults to name 560 length - defaults to 32,768 561 name 562 visibleLines - defaults to 25 563 564 %s 565 description 566 helptext 567 `, "\x1b[31;1mrequired attributes\x1b[0m", "\x1b[31;1moptional attributes\x1b[0m") 568 } 569 func DisplayCheckboxFieldDetails() (message string) { 570 return fmt.Sprintf(` 571 Allows users to select a True (checked) or False (unchecked) value. 572 573 %s 574 label - defaults to name 575 name 576 577 %s 578 description 579 helptext 580 defaultValue - defaults to unchecked or false 581 formula - defaultValue must be blask 582 formulaTreatBlanksAs - defaults to "BlankAsZero" 583 `, "\x1b[31;1mrequired attributes\x1b[0m", "\x1b[31;1moptional attributes\x1b[0m") 584 } 585 func DisplayDatetimeFieldDetails() (message string) { 586 return fmt.Sprintf(` 587 Allows users to enter a date and time. 588 589 %s 590 label - defaults to name 591 name 592 593 %s 594 description 595 helptext 596 defaultValue 597 required - defaults to false 598 formula - defaultValue must be blask 599 formulaTreatBlanksAs - defaults to "BlankAsZero" 600 `, "\x1b[31;1mrequired attributes\x1b[0m", "\x1b[31;1moptional attributes\x1b[0m") 601 } 602 func DisplayDoubleFieldDetails() (message string) { 603 return fmt.Sprintf(` 604 Allows users to enter any number. Leading zeros are removed. 605 606 %s 607 label - defaults to name 608 length - defaults to 18 609 name 610 precision - decimal places (defaults to 0) 611 scale - digits left of decimal (defaults to 18) 612 613 %s 614 description 615 helptext 616 required - defaults to false 617 unique - defaults to false 618 externalId - defaults to false 619 defaultValue 620 formula - defaultValue must be blask 621 formulaTreatBlanksAs - defaults to "BlankAsZero" 622 `, "\x1b[31;1mrequired attributes\x1b[0m", "\x1b[31;1moptional attributes\x1b[0m") 623 } 624 func DisplayCurrencyFieldDetails() (message string) { 625 return fmt.Sprintf(` 626 Allows users to enter a dollar or other currency amount and automatically formats the field as a currency amount. 627 628 %s 629 label - defaults to name 630 length - defaults to 18 631 name 632 precision - decimal places (defaults to 0) 633 scale - digits left of decimal (defaults to 18) 634 635 %s 636 description 637 helptext 638 required - defaults to false 639 defaultValue 640 formula - defaultValue must be blask 641 formulaTreatBlanksAs - defaults to "BlankAsZero" 642 `, "\x1b[31;1mrequired attributes\x1b[0m", "\x1b[31;1moptional attributes\x1b[0m") 643 } 644 func DisplayAutonumberFieldDetails() (message string) { 645 return fmt.Sprintf(` 646 A system-generated sequence number that uses a display format you define. The number is automatically incremented for each new record. 647 648 %s 649 label - defaults to name 650 name 651 displayFormat - defaults to "AN-{00000}" 652 startingNumber - defaults to 0 653 654 %s 655 description 656 helptext 657 externalId - defaults to false 658 `, "\x1b[31;1mrequired attributes\x1b[0m", "\x1b[31;1moptional attributes\x1b[0m") 659 } 660 func DisplayGeolocationFieldDetails() (message string) { 661 return fmt.Sprintf(` 662 Allows users to define locations. 663 664 %s 665 label - defaults to name 666 name 667 DisplayLocationInDecimal - defaults false 668 scale - defaults to 5 (number of decimals to the right) 669 670 %s 671 description 672 helptext 673 required - defaults to false 674 `, "\x1b[31;1mrequired attributes\x1b[0m", "\x1b[31;1moptional attributes\x1b[0m") 675 } 676 func DisplayLookupFieldDetails() (message string) { 677 return fmt.Sprintf(` 678 Creates a relationship that links this object to another object. 679 680 %s 681 label - defaults to name 682 name 683 referenceTo - Name of related object 684 relationshipName - defaults to referenceTo value 685 686 %s 687 description 688 helptext 689 required - defaults to false 690 relationShipLabel 691 `, "\x1b[31;1mrequired attributes\x1b[0m", "\x1b[31;1moptional attributes\x1b[0m") 692 } 693 func DisplayMasterDetailFieldDetails() (message string) { 694 return fmt.Sprintf(` 695 Creates a special type of parent-child relationship between this object (the child, or "detail") and another object (the parent, or "master") where: 696 The relationship field is required on all detail records. 697 The ownership and sharing of a detail record are determined by the master record. 698 When a user deletes the master record, all detail records are deleted. 699 You can create rollup summary fields on the master record to summarize the detail records. 700 701 %s 702 label - defaults to name 703 name 704 referenceTo - Name of related object 705 relationshipName - defaults to referenceTo value 706 707 %s 708 description 709 helptext 710 required - defaults to false 711 relationShipLabel 712 `, "\x1b[31;1mrequired attributes\x1b[0m", "\x1b[31;1moptional attributes\x1b[0m") 713 }