Read an object and its properties

View as Markdown

Given an object's type and ID (for example from a search), get its latest version, its files and all its property values, and turn the typed values into text.

Calls

GetObjectInfoAndProperties with:

Field Value
obj_id The object: type is the object type ID, item_id.internal_id the object ID
latest_version true for the latest version; false to read the version in doc_version
allow_checked_out Optional: true to get your own checked-out version when the object is checked out to you (the recipe leaves it false)

The response has object_info (ObjectVersionEx: title, version, files, check-out state) and property_array, a list of PropertyValue. A property value carries only the property definition ID; get the names from GetPropertyDefs (and cache them).

Each value is a TypedValue: type is the data type, and one field of the data oneof holds the value:

Data type Field of data Notes
DATATYPE_TEXT, DATATYPE_MULTI_LINE_TEXT text, multi_line_text
DATATYPE_INTEGER, DATATYPE_INTEGER64, DATATYPE_REAL_NUMBER integer, integer64, real_number
DATATYPE_BOOLEAN boolean
DATATYPE_DATE date A Timestamp at midnight UTC of the date
DATATYPE_TIMESTAMP timestamp UTC
DATATYPE_LOOKUP lookup value_list_item_info.obj_id (value list and item ID) and .name
DATATYPE_MULTI_SELECT_LOOKUP multi_select_lookup.values A list of lookups

An empty value has is_null_value true and no data field set.

Code

The recipe reads object 437 of object type 0 by default; pass another object ID and object type as arguments. format_value (FormatValue in C# and Go) turns a typed value into text: it checks is_null_value, then which member of the data oneof is set.

Python

def format_value(value: structs.TypedValue) -> str:
    """A typed value as text: lookups show their names and IDs."""
    if value.is_null_value:
        return "(empty)"
    data = value.data
    kind = data.WhichOneof("data")  # The member of the oneof that is set.
    if kind in ("text", "multi_line_text", "integer", "integer64", "real_number",
                "decimal_number"):
        return str(getattr(data, kind))
    if kind == "boolean":
        return "yes" if data.boolean else "no"
    if kind == "date":  # Midnight UTC of the day.
        return f"{data.date.ToDatetime():%Y-%m-%d}"
    if kind == "timestamp":
        return f"{data.timestamp.ToDatetime():%Y-%m-%d %H:%M:%S} UTC"
    if kind == "lookup":
        return _format_lookup(data.lookup)
    if kind == "multi_select_lookup":
        return "; ".join(_format_lookup(item) for item in data.multi_select_lookup.values)
    return f"({value.type})"


def _format_lookup(item: structs.Lookup) -> str:
    info = item.value_list_item_info
    return f"{info.name} (ID {info.obj_id.item_id.internal_id})"

Source: python/recipes/recipe_helpers.py

# Property names, by ID. A real application caches these (they change rarely).
property_defs = client.stub(IRPCPropertyDefs_pb2_grpc.IRPCPropertyDefsStub)
names = {p.id: p.name for p in property_defs.GetPropertyDefs(
    IRPCPropertyDefs_pb2.GetPropertyDefsRequest()).property_defs}

# The latest version and its properties in one call.
objects = client.stub(IRPCObjectOperations_pb2_grpc.IRPCObjectOperationsStub)
response = objects.GetObjectInfoAndProperties(
    IRPCObjectOperations_pb2.GetObjectInfoAndPropertiesRequest(
        obj_id=obj_id(object_type, item_id),
        latest_version=True))

print(describe(response.object_info))
for prop in response.property_array:
    name = names.get(prop.property_def, "?")
    print(f"  {name} ({prop.property_def}): {format_value(prop.value)}")

Source: python/recipes/read_object.py

C#

/// <summary>A typed value as text: lookups show their names and IDs.</summary>
public static string FormatValue(TypedValue value)
{
    if (value.IsNullValue)
        return "(empty)";
    var data = value.Data;
    return data.DataCase switch
    {
        TypedValueUnion.DataOneofCase.Text => data.Text,
        TypedValueUnion.DataOneofCase.MultiLineText => data.MultiLineText,
        TypedValueUnion.DataOneofCase.Integer => data.Integer.ToString(CultureInfo.InvariantCulture),
        TypedValueUnion.DataOneofCase.Integer64 => data.Integer64.ToString(CultureInfo.InvariantCulture),
        TypedValueUnion.DataOneofCase.RealNumber => data.RealNumber.ToString(CultureInfo.InvariantCulture),
        TypedValueUnion.DataOneofCase.DecimalNumber => data.DecimalNumber,
        TypedValueUnion.DataOneofCase.Boolean => data.Boolean ? "yes" : "no",
        // A date is midnight UTC of the day.
        TypedValueUnion.DataOneofCase.Date => data.Date.ToDateTime().ToString("yyyy-MM-dd"),
        TypedValueUnion.DataOneofCase.Timestamp =>
            data.Timestamp.ToDateTime().ToString("yyyy-MM-dd HH:mm:ss") + " UTC",
        TypedValueUnion.DataOneofCase.Lookup => FormatLookup(data.Lookup),
        TypedValueUnion.DataOneofCase.MultiSelectLookup =>
            string.Join("; ", data.MultiSelectLookup.Values.Select(FormatLookup)),
        _ => $"({value.Type})",
    };
}

private static string FormatLookup(Lookup lookup) =>
    $"{lookup.ValueListItemInfo.Name} (ID {lookup.ValueListItemInfo.ObjId.ItemId.InternalId})";

Source: csharp/Recipes/RecipeHelpers.cs

// Property names, by ID. A real application caches these (they change rarely).
var propertyDefs = new IRPCPropertyDefs.IRPCPropertyDefsClient(connection.CallInvoker);
var names = (await propertyDefs.GetPropertyDefsAsync(new GetPropertyDefsRequest()))
    .PropertyDefs.ToDictionary(p => p.Id, p => p.Name);

// The latest version and its properties in one call.
var objects = new IRPCObjectOperations.IRPCObjectOperationsClient(connection.CallInvoker);
var response = await objects.GetObjectInfoAndPropertiesAsync(new GetObjectInfoAndPropertiesRequest
{
    ObjId = ObjId(objectType, id),
    LatestVersion = true,
});

Console.WriteLine(RecipeHelpers.Describe(response.ObjectInfo));
foreach (var property in response.PropertyArray)
{
    string name = names.GetValueOrDefault(property.PropertyDef, "?");
    Console.WriteLine($"  {name} ({property.PropertyDef}): {RecipeHelpers.FormatValue(property.Value)}");
}

Source: csharp/Recipes/ReadObject.cs

Go


// FormatValue prints a typed value; lookups show their names and IDs. The value's Data holds
// one member of the TypedValueUnion oneof, as one of the TypedValueUnion_* wrapper types.
func FormatValue(value *mfiles.TypedValue) string {
	if value.GetIsNullValue() {
		return "(empty)"
	}
	switch data := value.GetData().GetData().(type) {
	case *mfiles.TypedValueUnion_Text:
		return data.Text
	case *mfiles.TypedValueUnion_MultiLineText:
		return data.MultiLineText
	case *mfiles.TypedValueUnion_Integer:
		return strconv.Itoa(int(data.Integer))
	case *mfiles.TypedValueUnion_Integer64:
		return strconv.FormatInt(data.Integer64, 10)
	case *mfiles.TypedValueUnion_RealNumber:
		return strconv.FormatFloat(data.RealNumber, 'g', -1, 64)
	case *mfiles.TypedValueUnion_DecimalNumber:
		return data.DecimalNumber
	case *mfiles.TypedValueUnion_Boolean:
		if data.Boolean {
			return "yes"
		}
		return "no"
	case *mfiles.TypedValueUnion_Date: // midnight UTC of the day
		return data.Date.AsTime().Format(time.DateOnly)
	case *mfiles.TypedValueUnion_Timestamp:
		return data.Timestamp.AsTime().Format(time.DateTime) + " UTC"
	case *mfiles.TypedValueUnion_Lookup:
		return formatLookup(data.Lookup)
	case *mfiles.TypedValueUnion_MultiSelectLookup:
		items := make([]string, 0, len(data.MultiSelectLookup.GetValues()))
		for _, lookup := range data.MultiSelectLookup.GetValues() {
			items = append(items, formatLookup(lookup))
		}
		return strings.Join(items, "; ")
	default:
		return fmt.Sprintf("(%s)", value.GetType())
	}
}

func formatLookup(lookup *mfiles.Lookup) string {
	item := lookup.GetValueListItemInfo()
	return fmt.Sprintf("%s (ID %d)", item.GetName(), item.GetObjId().GetItemId().GetInternalId())
}

Source: go/recipes/internal/recipe/recipe.go

// Property names, by ID. A real application caches these (they change rarely).
propertyDefs := mfiles.NewIRPCPropertyDefsClient(client.Conn)
defs, err := propertyDefs.GetPropertyDefs(ctx, &mfiles.GetPropertyDefsRequest{})
if err != nil {
	return err
}
names := map[int32]string{}
for _, def := range defs.GetPropertyDefs() {
	names[def.GetId()] = def.GetName()
}

// The latest version and its properties in one call.
objects := mfiles.NewIRPCObjectOperationsClient(client.Conn)
response, err := objects.GetObjectInfoAndProperties(ctx,
	&mfiles.GetObjectInfoAndPropertiesRequest{
		ObjId:         mfilesclient.ObjID(objectType, id),
		LatestVersion: true,
	})
if err != nil {
	return err
}

fmt.Println(recipe.Describe(response.GetObjectInfo()))
for _, property := range response.GetPropertyArray() {
	name, ok := names[property.GetPropertyDef()]
	if !ok {
		name = "?"
	}
	fmt.Printf("  %s (%d): %s\n", name, property.GetPropertyDef(),
		recipe.FormatValue(property.GetValue()))
}

Source: go/recipes/read_object/main.go

Output for a document of the Sample Vault:

Proposal 7732 - ESTT Corporation (IT) (ID 437, version 1, modified 2012-06-07 10:39:43 UTC)
  Name or title (0): Proposal 7732 - ESTT Corporation (IT)
  Last modified (21): 2012-06-07 10:39:43 UTC
  Single file (22): yes
  Last modified by (23): Andy Nash (ID 21)
  Status changed (24): 2012-06-07 10:39:43 UTC
  Size on server (this version) (30): 57344
  Class (100): Proposal (ID 5)
  Class groups (101): 1. PURCHASING AND SALES (ID 3)
  Document date (1002): 2005-03-02
  Keywords (1004): (empty)
  Description (1034): (empty)
  Document (1076): Order - Graphical Design (ID 293)
  Project (1078): Redesign of ESTT Marketing Material (ID 34)
  Customer (1079): ESTT Corporation (IT) (ID 141)
  Effective through (1096): 2007-08-01
  Accepted (1098): yes
  Proposal title (1102): Proposal 7732 - ESTT Corporation (IT)
  Proposal number (1103): 7732
  Created (20): 2012-06-07 10:39:31 UTC
  Created by (25): Andy Nash (ID 21)
  Size on server (all versions) (31): 57344
  Marked for archiving (32): no
  Object changed (89): 2012-06-07 10:39:43 UTC

Pitfalls

See also