session.queryObjects does not support secondary types - cmis

Reading this https://chemistry.apache.org/docs/cmis-samples/samples/properties/index.html#retrieving-properties, I thought it would be possible to retrieve secondary types using queryObjects method, but it does not. For example, I'm trying to get cm:author from Alfresco, it returns null. Here is my piece of code:
OperationContext oc = OperationContextUtils.createMaximumOperationContext();
ItemIterable<CmisObject> results = session.queryObjects(task.getCmisType(), where, false, oc);
...
Object value = cmisObject.getPropertyValue("cm:author");
Am I missing something?
P.S: I'm using Chemistry 1.0.0, CMIS 1.1, Binding: Browser
UPDATE:
Okay I found something interesting, In order to retrieve cm:author, I have to reload the cmisObject to make it work:
results = session.queryObjects("cmis:document", "IN_FOLDER('" + folder.getId() + "')", false, oc);
results.each { it ->
object = session.getObject(it.getId());
author = object.getPropertyValue("cm:author");
if(author != null) {
println object.getId() + " => " + author;
}
Bug?

First make sure cm:author is what you want. That is not the person who created the document node in Alfresco. That is an editable property that anyone can set to anything, and by default it is null.
If what you want is the actual username of the person who created the document node, you should use cmis:createdBy which is mapped to alfresco's cm:creator property.
Assuming cm:author is definitely what you want, you have two choices regarding how to get it. First, you can get it from the object. But in order to get it from the object you must first fetch the object. Your query returns QueryResult objects, not CmisObjects.
So you should do something like:
ItemIterable<QueryResult> results = session.query(queryString, false);
for (QueryResult qResult : results) {
String objectId = "";
PropertyData<?> propData = qResult.getPropertyById("cmis:objectId");
if (propData != null) {
objectId = (String) propData.getFirstValue();
}
CmisObject obj = session.getObject(session.createObjectId(objectId));
// Dump the object here
System.out.println("Author: " + obj.getPropertyValue("cm:author");
}
Your second option would be to get the property value from the query result. Your ability to do this depends on the query you ran. The author property is defined on an aspect, so you must do a join in order to get it back. The query might look something like:
queryString = "select content.cmis:name, content.cmis:objectId, author.cm:author from cmis:document content JOIN cm:author author ON content.cmis:objectId = author.cmis:objectId WHERE content.cmis:objectId is not null AND author.cm:author = 'Jeff'";
If you use that query, then you can grab the author using the QueryResult, like this:
System.out.println("Author: " + qResult.getPropertyValueByQueryName("author.cm:author"));
Hopefully that explains the difference between fetching the value from a query result and fetching a property value from the object itself.

Related

ServiceStack: Update<T>(...) produces 'duplicate entry'

I have tried reading the docs, but I don't get why the Update method produces a "Duplicate entry" MySQL error.
The docs says
In its most simple form, updating any model without any filters will update every field, except the Id which is used to filter the update to this specific record:
So I try it, and pass in an object, like below. A row with id 2 already exists.
using (var _db = _dbFactory.Open())
{
Customer coreObject = new Customer(...);
coreObject.Id = 2;
coreObject.ObjectName = "a changed value";
_db.Update<Customer>(coreObject); // <-- error "duplicate entry"
}
Yes, there are options using .Save and such, but what am I missing with the .Update? As I read it, it should use its Id property to update the row in the db, not insert a new row?
The issue with this method is that you're updating a generic object T but your Update API says to update the Concrete Customer type:
public void MyTestMethod<T>(T coreObject) where T : CoreObject
{
long id = 0;
using (var _db = _dbFactory.Open())
{
id = _db.Insert<T>(coreObject, selectIdentity: true);
if (DateTime.Now.Ticks == 0)
{
coreObject.Id = (uint)id;
_db.Delete(coreObject);
}
if (DateTime.Now.Ticks == 0)
{
_db.DeleteById<Customer>(id);
}
if (DateTime.Now.Ticks == 0)
{
coreObject.Id = (uint)id;
coreObject.ObjectName = "a changed value";
_db.Update<Customer>(coreObject);
}
}
}
Which OrmLite assumes that you're using a different/anonymous object to update the customer table, similar to:
db.Update<Customer>(new { Id = id, ObjectName = "a changed value", ... });
Which as it doesn't have a WHERE filter will attempt to update all rows with the same primary key.
What you instead want is to update the same entity, either passing in the Generic Type T or have it inferred by not passing in any type, e.g:
_db.Update<T>(coreObject);
_db.Update(coreObject);
Which will use OrmLite's behavior of updating entity by updating each field except for Primary Keys which it instead used in the WHERE expression to limit the update to only update that entity.
New Behavior in v5.1.1
To prevent accidental misuse like this I've added an Update API overload in this commit which will use the Primary Key as a filter when using an anonymous object to update an entity, so your previous usage:
_db.Update<Customer>(coreObject);
Will add the Primary Key to the WHERE filter instead of including it in the SET list. This change is available from v5.1.1 that's now available on MyGet.

Field index for queries not updating when value set programmatically

My module creates a custom content item through the controller:
private ContentItem createContentItem()
{
// Add the field
_contentDefinitionManager.AlterPartDefinition(
"TestType",
cfg => cfg
.WithField(
"NewField",
f => f
.OfType(typeof(BooleanField).Name)
.WithDisplayName("New Field"))
);
// Not sure if this is needed
_contentDefinitionManager.AlterTypeDefinition(
"TestType",
cfg => cfg
.WithPart("TestType")
);
// Create new TestType item
var newItem = _contentManager.New("TestType");
_contentManager.Create(TestItem, VersionOptions.Published);
// Set the added boolean field to true
BooleanField newField = ((dynamic)newItem).TestType.NewField as BooleanField;
newField.Value = true;
// Set title (as date created, for convenience)
var time = DateTime.Now.ToString("MM-dd-yyyy h:mm:ss tt", CultureInfo.InvariantCulture).Replace(':', '.');
newItem.As<TitlePart>().Title = time;
return newItem;
}
The end result of this is a new TestType item with a field that's set to true. Viewing the content item in the dashboard as well as examining ContentItemVersionRecord in the database confirms that the value was set correctly.
However, queries don't seem to work properly on fields that are set in this manner. I found the record IntegerFieldIndexRecord, which is what I assume projections use to fill query result pages. On this, the value of TestField remains at 0 (false), instead of 1 (true).
Going to the content item edit page and simply clicking 'save' updates IntegerFieldIndexRecord correctly, meaning that the value is now picked up by the query. How can the record be updated for field values set programmatically?
Relevant section of migration:
SchemaBuilder.CreateTable(typeof(TestTypePartRecord).Name, table => table
.ContentPartRecord()
);
ContentDefinitionManager.AlterTypeDefinition(
"TestType",
cfg => cfg
.DisplayedAs("Test Type")
.WithPart(typeof(TitlePart).Name)
.WithPart(typeof(ContainablePart).Name)
.WithPart(typeof(CommonPart).Name)
.WithPart(typeof(IdentityPart).Name)
);
Edit: The fix for this is to manually change the projection index record whenever changing a field value, using this call:
_fieldIndexService.Set(testResultItem.As<FieldIndexPart>(),
"TestType", // Resolves as TestTypePart, which holds the field
"newField",
"", // Not sure why value name should be empty, but whatever
true, // The value to be set goes here
typeof(bool));
In some cases a simple contentManager.Publish() won't do.
I've had a similar problem some time ago and actually implemented a simple helper service to tackle this problem; here's an excerpt:
public T GetStringFieldValues<T>(ContentPart contentPart, string fieldName)
{
var fieldIndexPart = contentPart.ContentItem.As<FieldIndexPart>();
var partName = contentPart.PartDefinition.Name;
return this.fieldIndexService.Get<T>(fieldIndexPart, partName, fieldName, string.Empty);
}
private void SetStringFieldValue(ContentPart contentPart, string fieldName, IEnumerable<int> ids)
{
var fieldIndexPart = contentPart.ContentItem.As<FieldIndexPart>();
var partName = contentPart.PartDefinition.Name;
var encodedValues = "{" + string.Join("},{", ids) + "}";
this.fieldIndexService.Set(fieldIndexPart, partName, fieldName, string.Empty, encodedValues, typeof(string));
}
I've actually built this for use with MediaLibrary- and ContentPicker fields (they encode their value as string internally), so it might not be suitable for the boolean field in your example.
But it can't be that hard to implement, just look at the existing drivers and handlers for those fields.
There are 2 ways to fix this:
1) Ensure the newly created item is getting published by calling ContentManager.Publish() as Orchard.Projections.Handlers.FieldIndexPartHandler listens to the publish event to update the FieldIndexPartRecord
2) use IFieldIndexService to update FieldIndexPartRecord manually, see implementation of Orchard.Projections.Handlers.FieldIndexPartHandler to get in idea how to do this
Hope this helps.
:edit
Due to calling Create(...Published) the ContentManager.Published() won't do anything as the item is already considered published.
You can do the following to force the publish logic to run:
bool itemPublished = newItem.VersionRecord.Published;
// unpublish item first when it is already published as ContentManager.Publish() internally first checks for published flag and when set it aborts silently
// -> this behaviour prevents calling publish listeners
if (itemPublished)
_contentManager.Unpublish(newItem);
// the following call will result in calls to IContentHandler.Publishing() / IContentHandler.Published()
_contentManager.Publish(newItem);
or just create the item as a draft and publish it when everything is setup correctly.

"Order By" When Retrieving From Acumatica Web Service API

I was wondering if there was a way to add an "Order By" clause when retrieving data from Acumatica through the Web Service API?
IN202500Content IN202500 = oScreen.IN202500GetSchema();
oScreen.IN202500Clear();
Command[] oCmd = new Command[] {IN202500.StockItemSummary.ServiceCommands.EveryInventoryID,
IN202500.StockItemSummary.InventoryID,
IN202500.StockItemSummary.Description,
IN202500.StockItemSummary.ItemStatus,
IN202500.GeneralSettingsItemDefaults.ItemClass,
IN202500.GeneralSettingsItemDefaults.LotSerialClass,
IN202500.PriceCostInfoPriceManagement.DefaultPrice,
};
Filter[] oFilter = new Filter[] {new Filter
{
Field = new Field {ObjectName = IN202500.StockItemSummary.InventoryID.ObjectName,
FieldName = "LastModifiedDateTime"},
Condition = FilterCondition.GreaterOrEqual,
Value = SyncDate
}
};
String[][] sReturn = oScreen.IN202500Export(oCmd, oFilter, iMaxRecords, true, false);
I would like to sort the results for example by DefaultPrice, so that I can retrieve the Top 200 most expensive items in my list (using iMaxRecords = 200 in this case)
I haven't seen any parameters that allows me to do the sorting yet.
I ran into this when I developed a round robin assignment system and the short answer is using the Acumatica API you cant do a sort on the results you have to do it outside of the API (This info came from a friend closely tied to the Acumatica product).
I came up with two options:
Query your DB directly... There are always reasons not to do this but it is much faster than pulling the result from the API and will allow you to bypass the BQL Acumatica uses and write an SQL statement that does EXACTLY what you want providing a result that is easier to work with than the jagged array Acumatica sends.
You can use some Linq and build a second array[][] that is sorted by price and then trim it to the top 200 (You would need all results from Acumatica first).
// This is rough but should get you there.
string[][] MaxPriceList = sReturn.OrderBy(innerArray =>
{
if () // This is a test to make sure the element is not null
{
decimal price;
if (//test decimal is not null))
return price;
}
return price.MaxValue;
}).Take(200).ToArray(); //Take 200 is a shot but might work

Linq queries and optionSet labels, missing formatted value

I'm doin a simple query linq to retrieve a label from an optionSet. Looks like the formatted value for the option set is missing. Someone knows why is not getting generated?
Best Regards
Sorry for the unclear post. I discovered the problem, and the reason of the missing key as formattedvalue.
The issue is with the way you retrieve the property. With this query:
var invoiceDetails = from d in xrmService.InvoiceSet
where d.InvoiceId.Value.Equals(invId)
select new
{
name = d.Name,
paymenttermscode = d.PaymentTermsCode
}
I was retrieving the correct int value for the option set, but what i needed was only the text. I changed the query this way:
var invoiceDetails = from d in xrmService.InvoiceSet
where d.InvoiceId.Value.Equals(invId)
select new
{
name = d.Name,
paymenttermscode = d.FormattedValues["paymenttermscode"]
}
In this case I had an error stating that the key was not present. After many attempts, i tried to pass both the key value and the option set text, and that attempt worked just fine.
var invoiceDetails = from d in xrmService.InvoiceSet
where d.InvoiceId.Value.Equals(invId)
select new
{
name = d.Name,
paymenttermscode = d.PaymentTermsCode,
paymenttermscodeValue = d.FormattedValues["paymenttermscode"]
}
My guess is that to retrieve the correct text associated to that option set, in that specific entity, you need to retrieve the int value too.
I hope this will be helpful.
Best Regards
You're question is rather confusing for a couple reasons. I'm going to assume that what you mean when you say you're trying to "retrieve a label from an OptionSet" is that you're attempting to get the Text Value of a particular OptionSetValue and you're not querying the OptionSetMetadata directly to retrieve the actual LocalizedLabels text value. I'm also assuming "formatted value for the option set is missing" is referring to the FormattedValues collection. If these assumptions are correct, I refer you to this: CRM 2011 - Retrieving FormattedValues from joined entity
The option set metadata has to be queried.
Here is an extension method that I wrote:
public static class OrganizationServiceHelper
{
public static string GetOptionSetLabel(this IOrganizationService service, string optionSetName, int optionSetValue)
{
RetrieveOptionSetRequest retrieve = new RetrieveOptionSetRequest
{
Name = optionSetName
};
try
{
RetrieveOptionSetResponse response = (RetrieveOptionSetResponse)service.Execute(retrieve);
OptionSetMetadata metaData = (OptionSetMetadata)response.OptionSetMetadata;
return metaData.Options
.Where(o => o.Value == optionSetValue)
.Select(o => o.Label.UserLocalizedLabel.Label)
.FirstOrDefault();
}
catch { }
return null;
}
}
RetrieveOptionSetRequest and RetrieveOptionSetResponse are on Microsoft.Xrm.Sdk.Messages.
Call it like this:
string label = service.GetOptionSetLabel("wim_continent", 102730000);
If you are going to be querying the same option set multiple times, I recommend that you write a method that returns the OptionSetMetadata instead of the label; then query the OptionSetMetadata locally. Calling the above extension method multiple times will result in the same query being executed over and over.

Retrieve related entities of each, using RetrieveMultipleRequest

I'm trying to retrieve a list of entities from CRM, but I'd like to get each one with the related entities. So far, I've the following code:
FilterExpression filterExpression = new FilterExpression();
ConditionExpression condition = new ConditionExpression(Constants.ModifiedOnAttribute, ConditionOperator.GreaterEqual, lastSync);
filterExpression.AddCondition(condition);
QueryExpression query = new QueryExpression()
{
EntityName = entityName,
ColumnSet = new ColumnSet(attributesMetadata.Select(att => att.Name).ToArray<string>()),
Criteria = filterExpression,
Distinct = false,
NoLock = true
};
RetrieveMultipleRequest multipleRequest = new RetrieveMultipleRequest();
multipleRequest.Query = queryExpression;
RetrieveMultipleResponse response = (RetrieveMultipleResponse)proxy.Execute(multipleRequest);
In the variable response, I can see the EntityCollection attribute, but inside, Related entities always come empty.
I'd like to know if it is possible to retrieve the set of a given entities, with the related entities, using RetrieveMultipleRequest, rather than go one by one using RetrieveRequest.
One approach to retreive related entities data - adding LinkEntities to your query. Example below will make you an idea how to make this:
LinkEntity linkEntity = new LinkEntity("email", "new_emails", "activityid", "new_relatedemail", JoinOperator.Inner);
linkEntity.Columns.AddColumn("versionnumber");
linkEntity.Columns.AddColumn("new_emailsid");
linkEntity.EntityAlias = "related";
query = new QueryExpression("email");
query.ColumnSet.AddColumn("activityid");
query.ColumnSet.AddColumn("versionnumber");
query.Criteria.AddCondition("modifiedon", ConditionOperator.NotNull);
query.LinkEntities.Add(linkEntity);
And then you can access attributes from related entities using EntityAlias you specified above:
foreach (Entity entity in entities.Entities)
{
if ((long)(entity["related.versionnumber"] as AliasedValue).Value > 0)
{
stop = false;
}
}
The RetrieveMultipleRequest is for returning multiple instances of a particular type of entity. I have spent a year using the CRM SDK from C# and I have found no way of populating those related entity collections in a single query. This basically leaves you with two options:
Use the AliasedValue as SergeyS recommends. Remember when querying 1:Many relationships, be aware that you could be returning multiple results for the same parent entity. This is what I use most of the time.
Perform a second query for each relationship you want access to. You'll probably get better performance if you can use an IN statement in your second query, based on the results of the first, rather than performing a separate query for each result of the first.
Below is some pseudo code to show the difference.
var contacts = GetContacts();
// One Request to get the cars for the contacts
var cars = GetCarsWhereContactIdIn(contacts.Select( c => c.new_ContactId));
foreach(var c in contacts){
c.new_Cars.AddRange(cars.where(car => car.new_contactId = c.ContactId));
}
// Verses
var contacts = GetContacts();
foreach(var c in contacts){
// One Request for each contact
c.new_Cars.AddRange(GetCarsForContact(c.ContactId));
}

Resources