Read an object and its properties
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
- Check the oneof, not the type. Read the value from the field that
dataactually has set (WhichOneofin Python), and checkis_null_valuefirst. - Lookup items can be in any value list. The
obj_id.typeof a lookup is the value list or object type; in proto3 a 0 (the Document type) is not sent, so an unsettypemeans 0. - Dates have no time zone. A
DATATYPE_DATEvalue is a date; print it in UTC, or it can shift by a day. - Not found. An object that does not exist, or that the user cannot see, fails with
0x8004000B"Not found." See Handle errors. - The property list can contain properties that the object's class does not list (1076 above), and it is not sorted by property ID.