Skip to content

Commit

Permalink
Feat: Set-DataverseRecord support for upserts using AK
Browse files Browse the repository at this point in the history
  • Loading branch information
rnwood committed Dec 29, 2024
1 parent 698e7b8 commit 47129e9
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public class SetDataverseRecordCmdlet : CustomLogicBypassableOrganizationService
[Parameter(Mandatory = false, ValueFromPipelineByPropertyName = true, HelpMessage = "ID of record to be created or updated.")]
public Guid Id { get; set; }

[Parameter(Mandatory = false, HelpMessage = "List of list of column names that identify an existing record to update based on the values of those columns in the InputObject. These are used if a record with and Id matching the value of the Id cannot be found. The first list that returns a match is used. e.g. (\"firstname\", \"lastname\"), \"fullname\" will try to find an existing record based on the firstname AND listname from the InputObject and if not found it will try by fullname. Not supported with -Upsert")]
[Parameter(Mandatory = false, HelpMessage = "List of list of column names that identify an existing record to update based on the values of those columns in the InputObject. For update/create these are used if a record with and Id matching the value of the Id cannot be found. The first list that returns a match is used. e.g. (\"firstname\", \"lastname\"), \"fullname\" will try to find an existing record based on the firstname AND listname from the InputObject and if not found it will try by fullname. For upsert only a single list is allowed and it must match the properties of an alternate key defined on the table.")]
public string[][] MatchOn { get; set; }

[Parameter(HelpMessage = "If specified, the InputObject is written to the pipeline with an Id property set indicating the primary key of the affected record (even if nothing was updated).")]
Expand Down Expand Up @@ -110,7 +110,7 @@ private object FormatValue(object value)
if (value is EntityReference er)
{
return $"{er.LogicalName}:{er.Id}";
}
}

if (value is Entity en)
{
Expand Down Expand Up @@ -432,13 +432,18 @@ private bool UpsertRecord(EntityMetadata entityMetadata, Entity target)
{
bool result = true;

if (NoCreate || NoUpdate || MatchOn != null)
if (NoCreate || NoUpdate)
{
throw new ArgumentException("-NoCreate, -NoUpdate and -MatchOn are not supported with -Upsert");
throw new ArgumentException("-NoCreate and -NoUpdate are not supported with -Upsert");
}

if (entityMetadata.IsIntersect.GetValueOrDefault())
{
if (MatchOn != null)
{
throw new ArgumentException("-MatchOn is not supported for -Upsert of M:M");
}

ManyToManyRelationshipMetadata manyToManyRelationshipMetadata = entityMetadata.ManyToManyRelationships[0];

EntityReference record1 = new EntityReference(manyToManyRelationshipMetadata.Entity1LogicalName,
Expand Down Expand Up @@ -487,6 +492,28 @@ private bool UpsertRecord(EntityMetadata entityMetadata, Entity target)
else
{
Entity targetUpdate = new Entity(target.LogicalName) { Id = target.Id };

if (MatchOn != null)
{
if (MatchOn.Length > 1)
{
throw new NotSupportedException("MatchOn must only have a single array when used with Upsert");
}

var key = entityMetadataFactory.GetMetadata(target.LogicalName).Keys.FirstOrDefault(k => k.KeyAttributes.Length == MatchOn[0].Length && k.KeyAttributes.All(a => MatchOn[0].Contains(a)));
if (key == null)
{
throw new ArgumentException($"MatchOn must match a key that is defined on the table");
}

targetUpdate.KeyAttributes = new KeyAttributeCollection();

foreach (var matchOnField in MatchOn[0])
{
targetUpdate.KeyAttributes.Add(matchOnField, target.GetAttributeValue<object>(matchOnField));
}
}

targetUpdate.Attributes.AddRange(target.Attributes.Where(a => !dontUpdateDirectlyColumnNames.Contains(a.Key, StringComparer.OrdinalIgnoreCase)));

string columnSummary = GetColumnSummary(targetUpdate);
Expand All @@ -498,29 +525,29 @@ private bool UpsertRecord(EntityMetadata entityMetadata, Entity target)

if (_nextBatchItems != null)
{
if (target.Id == Guid.Empty)
if (targetUpdate.Id == Guid.Empty && targetUpdate.KeyAttributes.Count == 0)
{
targetUpdate.Id = Guid.NewGuid();
}

var inputObject = InputObject;

WriteVerbose(string.Format("Added upsert of new record {0}:{1} to batch - columns:\n{2}", TableName, targetUpdate.Id, columnSummary));
QueueBatchItem(new BatchItem(InputObject, request, (response) => { UpsertCompletion(targetUpdate, inputObject, columnSummary, (UpsertResponse)response); }), CallerId);
WriteVerbose(string.Format("Added upsert of new record {0}:{1} to batch - columns:\n{2}", TableName, GetKeySummary(targetUpdate), columnSummary));
QueueBatchItem(new BatchItem(InputObject, request, (response) => { UpsertCompletion(targetUpdate, inputObject, (UpsertResponse)response); }), CallerId);
}
else
{
if (ShouldProcess(string.Format("Upsert record {0}:{1} columns:\n{2}", TableName, targetUpdate.Id, columnSummary)))
if (ShouldProcess(string.Format("Upsert record {0}:{1} columns:\n{2}", TableName, GetKeySummary(targetUpdate), columnSummary)))
{
try
{
UpsertResponse response = (UpsertResponse)Connection.Execute(request);
UpsertCompletion(targetUpdate, InputObject, columnSummary, response);
UpsertCompletion(targetUpdate, InputObject, response);
}
catch (Exception e)
{
result = false;
WriteError(new ErrorRecord(new Exception(string.Format("Error creating record {0}:{1} {2}, columns: {3}", TableName, targetUpdate.Id, e.Message, columnSummary), e), null, ErrorCategory.InvalidResult, InputObject));
WriteError(new ErrorRecord(new Exception(string.Format("Error creating record {0}:{1} {2}, columns: {3}", TableName, GetKeySummary(targetUpdate), e.Message, columnSummary), e), null, ErrorCategory.InvalidResult, InputObject));
}
}
}
Expand All @@ -529,6 +556,21 @@ private bool UpsertRecord(EntityMetadata entityMetadata, Entity target)
return result;
}

private static string GetKeySummary(Entity record)
{
if (record.Id != Guid.Empty)
{
return record.Id.ToString();
}

if (record.KeyAttributes.Any())
{
return string.Join(",", record.KeyAttributes.Select(kvp => $"{kvp.Key}={kvp.Value}"));
}

return "<No ID>";
}

private void AssociateUpsertGetIdCompletion(OrganizationResponse response, PSObject inputObject)
{
Guid id = ((RetrieveMultipleResponse)response).EntityCollection.Entities.Single().Id;
Expand All @@ -542,24 +584,26 @@ private void AssociateUpsertGetIdCompletion(OrganizationResponse response, PSObj
WriteObject(inputObject);
}

private void UpsertCompletion(Entity targetUpdate, PSObject inputObject, string columnSummary, UpsertResponse response)
private void UpsertCompletion(Entity targetUpdate, PSObject inputObject, UpsertResponse response)
{
Guid id = response.Target.Id;
targetUpdate.Id = response.Target.Id;

if (inputObject.Properties.Any(p => p.Name.Equals("Id", StringComparison.OrdinalIgnoreCase)))
{
inputObject.Properties.Remove("Id");
}

inputObject.Properties.Add(new PSNoteProperty("Id", id));
inputObject.Properties.Add(new PSNoteProperty("Id", targetUpdate.Id));

string columnSummary = GetColumnSummary(targetUpdate);

if (response.RecordCreated)
{
WriteVerbose(string.Format("Upsert created new record {0}:{1} columns:\n{2}", TableName, targetUpdate.Id, columnSummary));
WriteVerbose(string.Format("Upsert created new record {0}:{1} columns:\n{2}", TableName, GetKeySummary(targetUpdate), columnSummary));
}
else
{
WriteVerbose(string.Format("Upsert updated existing record {0}:{1} columns:\n{2}", TableName, targetUpdate.Id, columnSummary));
WriteVerbose(string.Format("Upsert updated existing record {0}:{1} columns:\n{2}", TableName, GetKeySummary(targetUpdate), columnSummary));
}

if (PassThru.IsPresent)
Expand Down Expand Up @@ -898,16 +942,6 @@ private Entity GetExistingRecord(EntityMetadata entityMetadata, Entity target)
QueryByAttribute matchOnQuery = new QueryByAttribute(TableName);
matchOnQuery.TopCount = 2;

if (entityMetadata.Attributes.Any(a => string.Equals(a.LogicalName, "statecode", StringComparison.OrdinalIgnoreCase)))
{
matchOnQuery.AddAttributeValue("statecode", 0);
}

if (entityMetadata.Attributes.Any(a => string.Equals(a.LogicalName, "isdisabled", StringComparison.OrdinalIgnoreCase)))
{
matchOnQuery.AddAttributeValue("isdisabled", false);
}

foreach (string matchOnColumn in matchOnColumnList)
{
object queryValue = target.GetAttributeValue<object>(matchOnColumn);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@

</Target>

<Target Name="BuildHelp" DependsOnTargets="BuildFrameworkSpecifics" AfterTargets="Build">
<Target Name="BuildHelp" DependsOnTargets="BuildFrameworkSpecifics" AfterTargets="Build" Inputs="$(ProjectDir)/docs" Outputs="$(TargetDir)/en-GB">
<Message Text="Building help to $(TargetDir)" Importance="high" />
<Message Text="pwsh -file &quot;$(ProjectDir)updatehelp.ps1&quot; -projectdir &quot;$(ProjectDir.TrimEnd(&quot;\\&quot;))&quot; -outdir &quot;$(TargetDir.TrimEnd(&quot;\\&quot;))&quot;" Importance="high" />
<Exec Command="pwsh -file &quot;$(ProjectDir)updatehelp.ps1&quot; -projectdir &quot;$(ProjectDir.TrimEnd(&quot;\\&quot;))&quot; -outdir &quot;$(TargetDir.TrimEnd(&quot;\\&quot;))&quot;" />
Expand Down
4 changes: 2 additions & 2 deletions Rnwood.Dataverse.Data.PowerShell/docs/Set-DataverseRecord.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,11 +163,11 @@ Accept wildcard characters: False
### -MatchOn
List of list of field names that identify an existing record to update based on the values of those fields in the InputObject.
These are used if a record with and Id matching the value of the Id cannot be found.
For create/update these are used if a record with and Id matching the value of the Id cannot be found.
The first list that returns a match is used.
e.g.
("firstname", "lastname"), "fullname" will try to find an existing record based on the firstname AND listname from the InputObject and if not found it will try by fullname.
Not supported with -Upsert
For upsert only a single list is allowed and it must match the properties of an alternate key defined on the table.
```yaml
Type: String[][]
Expand Down

0 comments on commit 47129e9

Please sign in to comment.