From 5ca3c92bca8ad9456cddd061b84cdee70a23161e Mon Sep 17 00:00:00 2001 From: Mark Carrington <31017244+MarkMpn@users.noreply.github.com> Date: Mon, 3 Jun 2024 08:32:27 +0100 Subject: [PATCH] Added recycle bin access via `bin` schema Fixes #471 --- .../AttributeMetadataCache.cs | 47 ++++++++++++++++++- .../ExecutionPlan/HashMatchAggregateNode.cs | 2 +- .../ExecutionPlanBuilder.cs | 11 ++++- .../IAttributeMetadataCache.cs | 5 ++ MarkMpn.Sql4Cds.Engine/MetaMetadataCache.cs | 3 ++ .../Autocomplete/Autocomplete.cs | 46 ++++++++++++------ .../Connection/CachedMetadata.cs | 2 + .../ObjectExplorer/ObjectExplorerHandler.cs | 43 +++++++++++++++++ MarkMpn.Sql4Cds.XTB/Autocomplete.cs | 46 ++++++++++++------ .../FetchXml2SqlSettingsForm.cs | 2 + MarkMpn.Sql4Cds.XTB/ObjectExplorer.cs | 47 ++++++++++++++++--- MarkMpn.Sql4Cds.XTB/SharedMetadataCache.cs | 10 ++++ 12 files changed, 225 insertions(+), 39 deletions(-) diff --git a/MarkMpn.Sql4Cds.Engine/AttributeMetadataCache.cs b/MarkMpn.Sql4Cds.Engine/AttributeMetadataCache.cs index f13e619b..095e7104 100644 --- a/MarkMpn.Sql4Cds.Engine/AttributeMetadataCache.cs +++ b/MarkMpn.Sql4Cds.Engine/AttributeMetadataCache.cs @@ -2,6 +2,7 @@ using Microsoft.Xrm.Sdk.Messages; using Microsoft.Xrm.Sdk.Metadata; using Microsoft.Xrm.Sdk.Metadata.Query; +using Microsoft.Xrm.Sdk.Query; using System; using System.Collections.Generic; using System.Linq; @@ -21,6 +22,7 @@ public class AttributeMetadataCache : IAttributeMetadataCache private readonly IDictionary _minimalMetadata; private readonly ISet _minimalLoading; private readonly IDictionary _invalidEntities; + private readonly Lazy _recycleBinEntities; /// /// Creates a new @@ -34,6 +36,46 @@ public AttributeMetadataCache(IOrganizationService org) _minimalMetadata = new Dictionary(StringComparer.OrdinalIgnoreCase); _minimalLoading = new HashSet(StringComparer.OrdinalIgnoreCase); _invalidEntities = new Dictionary(StringComparer.OrdinalIgnoreCase); + _recycleBinEntities = new Lazy(() => + { + // Check the recyclebinconfig entity exists + try + { + _ = this["recyclebinconfig"]; + } + catch + { + return null; + } + + // https://learn.microsoft.com/en-us/power-apps/developer/data-platform/restore-deleted-records?tabs=sdk#detect-which-tables-are-enabled-for-recycle-bin + var qry = new FetchExpression(@" + + + + + + + + + + + + "); + + var resp = _org.RetrieveMultiple(qry); + return resp.Entities + .Select(e => e.GetAttributeValue("entity.logicalname").Value as string) + .ToArray(); + }); } /// @@ -118,6 +160,7 @@ public bool TryGetValue(string logicalName, out EntityMetadata metadata) return false; } + /// public bool TryGetMinimalData(string logicalName, out EntityMetadata metadata) { if (_metadata.TryGetValue(logicalName, out metadata)) @@ -202,9 +245,11 @@ public bool TryGetMinimalData(string logicalName, out EntityMetadata metadata) } return false; - } + /// + public string[] RecycleBinEntities => _recycleBinEntities.Value; + public event EventHandler MetadataLoading; protected void OnMetadataLoading(MetadataLoadingEventArgs args) diff --git a/MarkMpn.Sql4Cds.Engine/ExecutionPlan/HashMatchAggregateNode.cs b/MarkMpn.Sql4Cds.Engine/ExecutionPlan/HashMatchAggregateNode.cs index 6cc3b057..7c85b61b 100644 --- a/MarkMpn.Sql4Cds.Engine/ExecutionPlan/HashMatchAggregateNode.cs +++ b/MarkMpn.Sql4Cds.Engine/ExecutionPlan/HashMatchAggregateNode.cs @@ -279,7 +279,7 @@ Source is FetchXmlScan fetch && var metadata = context.DataSources[fetchXml.DataSource].Metadata; // Aggregates are not supported on archive data - if (fetchXml.FetchXml.DataSource != null) + if (fetchXml.FetchXml.DataSource == "retained") canUseFetchXmlAggregate = false; // FetchXML is translated to QueryExpression for virtual entities, which doesn't support aggregates diff --git a/MarkMpn.Sql4Cds.Engine/ExecutionPlanBuilder.cs b/MarkMpn.Sql4Cds.Engine/ExecutionPlanBuilder.cs index 612e5ff5..718e7c14 100644 --- a/MarkMpn.Sql4Cds.Engine/ExecutionPlanBuilder.cs +++ b/MarkMpn.Sql4Cds.Engine/ExecutionPlanBuilder.cs @@ -4257,7 +4257,8 @@ private IDataExecutionPlanNodeInternal ConvertTableReference(TableReference refe if (!String.IsNullOrEmpty(table.SchemaObject.SchemaIdentifier?.Value) && !table.SchemaObject.SchemaIdentifier.Value.Equals("dbo", StringComparison.OrdinalIgnoreCase) && - !table.SchemaObject.SchemaIdentifier.Value.Equals("archive", StringComparison.OrdinalIgnoreCase)) + !table.SchemaObject.SchemaIdentifier.Value.Equals("archive", StringComparison.OrdinalIgnoreCase) && + !(table.SchemaObject.SchemaIdentifier.Value.Equals("bin", StringComparison.OrdinalIgnoreCase) && dataSource.Metadata.RecycleBinEntities != null)) throw new NotSupportedQueryFragmentException(Sql4CdsError.InvalidObjectName(table.SchemaObject)); // Validate the entity name @@ -4309,6 +4310,14 @@ private IDataExecutionPlanNodeInternal ConvertTableReference(TableReference refe fetchXmlScan.FetchXml.DataSource = "retained"; } + // Check if this should be using the recycle bin table + else if (table.SchemaObject.SchemaIdentifier?.Value.Equals("bin", StringComparison.OrdinalIgnoreCase) == true) + { + if (!dataSource.Metadata.RecycleBinEntities.Contains(meta.LogicalName)) + throw new NotSupportedQueryFragmentException(Sql4CdsError.InvalidObjectName(table.SchemaObject)) { Suggestion = "Ensure restoring of deleted records is enabled for this table - see https://learn.microsoft.com/en-us/power-platform/admin/restore-deleted-table-records?WT.mc_id=DX-MVP-5004203" }; + + fetchXmlScan.FetchXml.DataSource = "bin"; + } return fetchXmlScan; } diff --git a/MarkMpn.Sql4Cds.Engine/IAttributeMetadataCache.cs b/MarkMpn.Sql4Cds.Engine/IAttributeMetadataCache.cs index 47a68cee..007293cd 100644 --- a/MarkMpn.Sql4Cds.Engine/IAttributeMetadataCache.cs +++ b/MarkMpn.Sql4Cds.Engine/IAttributeMetadataCache.cs @@ -83,5 +83,10 @@ public interface IAttributeMetadataCache /// /// bool TryGetMinimalData(string logicalName, out EntityMetadata metadata); + + /// + /// Returns a list of entity logical names that are enabled for recycle bin access + /// + string[] RecycleBinEntities { get; } } } diff --git a/MarkMpn.Sql4Cds.Engine/MetaMetadataCache.cs b/MarkMpn.Sql4Cds.Engine/MetaMetadataCache.cs index dbbd6ef3..5b7ad405 100644 --- a/MarkMpn.Sql4Cds.Engine/MetaMetadataCache.cs +++ b/MarkMpn.Sql4Cds.Engine/MetaMetadataCache.cs @@ -301,5 +301,8 @@ public bool TryGetMinimalData(string logicalName, out EntityMetadata metadata) return _inner.TryGetMinimalData(logicalName, out metadata); } + + /// + public string[] RecycleBinEntities => _inner.RecycleBinEntities; } } diff --git a/MarkMpn.Sql4Cds.LanguageServer/Autocomplete/Autocomplete.cs b/MarkMpn.Sql4Cds.LanguageServer/Autocomplete/Autocomplete.cs index 35d5c6eb..bb4f7d6b 100644 --- a/MarkMpn.Sql4Cds.LanguageServer/Autocomplete/Autocomplete.cs +++ b/MarkMpn.Sql4Cds.LanguageServer/Autocomplete/Autocomplete.cs @@ -551,6 +551,12 @@ public IEnumerable GetSuggestions(string text, int pos) schemaName == "archive" && (e.IsArchivalEnabled == true || e.IsRetentionEnabled == true) ) + || + ( + schemaName == "bin" && + instance.Metadata.RecycleBinEntities != null && + instance.Metadata.RecycleBinEntities.Contains(e.LogicalName) + ) ) ); @@ -770,6 +776,7 @@ private IEnumerable AutocompleteTableName(string currentWor } else if (TryParseTableName(currentWord, out var instanceName, out var schemaName, out var tableName, out var parts, out var lastPartLength)) { + _dataSources.TryGetValue(instanceName, out var instance); var lastPart = tableName; if (parts == 1) @@ -781,18 +788,18 @@ private IEnumerable AutocompleteTableName(string currentWor if (parts == 1 || (parts == 2 && _dataSources.ContainsKey(schemaName))) { // Could be a schema name - if ("dbo".StartsWith(lastPart, StringComparison.OrdinalIgnoreCase)) - list.Add(new SchemaAutocompleteItem("dbo", lastPartLength)); + var schemaNames = (IEnumerable)new[] { "dbo", "archive", "metadata" }; + if (instance?.Metadata?.RecycleBinEntities != null) + schemaNames = schemaNames.Append("bin"); - if ("archive".StartsWith(lastPart, StringComparison.OrdinalIgnoreCase)) - list.Add(new SchemaAutocompleteItem("archive", lastPartLength)); + schemaNames = schemaNames.Where(s => s.StartsWith(lastPart, StringComparison.OrdinalIgnoreCase)); - if ("metadata".StartsWith(lastPart, StringComparison.OrdinalIgnoreCase)) - list.Add(new SchemaAutocompleteItem("metadata", lastPartLength)); + foreach (var schema in schemaNames) + list.Add(new SchemaAutocompleteItem(schema, lastPartLength)); } // Could be a table name - if (_dataSources.TryGetValue(instanceName, out var instance) && instance.Entities != null) + if (instance?.Entities != null) { IEnumerable entities; IEnumerable messages = Array.Empty(); @@ -818,6 +825,11 @@ private IEnumerable AutocompleteTableName(string currentWor messages = instance.Messages.GetAllMessages(); } } + else if (schemaName.Equals("bin", StringComparison.OrdinalIgnoreCase)) + { + // Suggest tables that are enabled for the recycle bin + entities = instance.Entities.Where(e => instance.Metadata.RecycleBinEntities != null && instance.Metadata.RecycleBinEntities.Contains(e.LogicalName)); + } else { entities = Array.Empty(); @@ -854,6 +866,7 @@ private IEnumerable AutocompleteSprocName(string currentWor } else if (TryParseTableName(currentWord, out var instanceName, out var schemaName, out var tableName, out var parts, out var lastPartLength)) { + _dataSources.TryGetValue(instanceName, out var instance); var lastPart = tableName; if (parts == 1) @@ -865,18 +878,18 @@ private IEnumerable AutocompleteSprocName(string currentWor if (parts == 1 || parts == 2) { // Could be a schema name - if ("dbo".StartsWith(lastPart, StringComparison.OrdinalIgnoreCase)) - list.Add(new SchemaAutocompleteItem("dbo", lastPartLength)); + var schemaNames = (IEnumerable)new[] { "dbo", "archive", "metadata" }; + if (instance?.Metadata?.RecycleBinEntities != null) + schemaNames = schemaNames.Append("bin"); - if ("archive".StartsWith(lastPart, StringComparison.OrdinalIgnoreCase)) - list.Add(new SchemaAutocompleteItem("archive", lastPartLength)); + schemaNames = schemaNames.Where(s => s.StartsWith(lastPart, StringComparison.OrdinalIgnoreCase)); - if ("metadata".StartsWith(lastPart, StringComparison.OrdinalIgnoreCase)) - list.Add(new SchemaAutocompleteItem("metadata", lastPartLength)); + foreach (var schema in schemaNames) + list.Add(new SchemaAutocompleteItem(schema, lastPartLength)); } // Could be a sproc name - if (schemaName.Equals("dbo", StringComparison.OrdinalIgnoreCase) && _dataSources.TryGetValue(instanceName, out var instance) && instance.Messages != null) + if (schemaName.Equals("dbo", StringComparison.OrdinalIgnoreCase) && instance?.Messages != null) list.AddRange(instance.Messages.GetAllMessages().Where(x => x.IsValidAsStoredProcedure()).Select(e => new SprocAutocompleteItem(e, lastPartLength))); } @@ -1498,7 +1511,10 @@ public override string ToolTipTitle public override string ToolTipText { - get => Text == "metadata" ? "Schema containing the metadata information" : "Schema containing the data tables"; + get => Text == "metadata" ? "Schema containing the metadata information" : + Text == "archive" ? "Schema containing long-term retention tables" : + Text == "bin" ? "Schema containing recycle bin tables" : + "Schema containing the data tables"; set => base.ToolTipText = value; } } diff --git a/MarkMpn.Sql4Cds.LanguageServer/Connection/CachedMetadata.cs b/MarkMpn.Sql4Cds.LanguageServer/Connection/CachedMetadata.cs index d26921b3..c948a10a 100644 --- a/MarkMpn.Sql4Cds.LanguageServer/Connection/CachedMetadata.cs +++ b/MarkMpn.Sql4Cds.LanguageServer/Connection/CachedMetadata.cs @@ -91,6 +91,8 @@ public bool TryGetValue(string logicalName, out EntityMetadata metadata) return _defaultCache.TryGetValue(logicalName, out metadata); } + public string[] RecycleBinEntities => _defaultCache.RecycleBinEntities; + public EntityMetadata[] GetAutocompleteEntities() { if (_cacheUnavailable && _autocompleteCache == null) diff --git a/MarkMpn.Sql4Cds.LanguageServer/ObjectExplorer/ObjectExplorerHandler.cs b/MarkMpn.Sql4Cds.LanguageServer/ObjectExplorer/ObjectExplorerHandler.cs index d27b8067..9bf7985c 100644 --- a/MarkMpn.Sql4Cds.LanguageServer/ObjectExplorer/ObjectExplorerHandler.cs +++ b/MarkMpn.Sql4Cds.LanguageServer/ObjectExplorer/ObjectExplorerHandler.cs @@ -113,6 +113,17 @@ public bool HandleExpand(ExpandParams request) }); } + if (session.DataSource.Metadata.RecycleBinEntities != null) + { + nodes.Add(new NodeInfo + { + IsLeaf = false, + Label = "Recycle Bin", + NodePath = request.NodePath + "/Bin", + NodeType = "Folder", + }); + } + nodes.Add(new NodeInfo { IsLeaf = false, @@ -205,6 +216,26 @@ public bool HandleExpand(ExpandParams request) } } } + else if (url.AbsolutePath == "/Bin") + { + foreach (var entity in session.DataSource.Metadata.RecycleBinEntities) + { + nodes.Add(new NodeInfo + { + IsLeaf = false, + Label = entity, + NodePath = request.NodePath + "/" + entity, + NodeType = "Table", + Metadata = new ObjectMetadata + { + Urn = request.NodePath + "/" + entity, + MetadataType = MetadataType.Table, + Schema = "bin", + Name = entity + } + }); + } + } else if (url.AbsolutePath.StartsWith("/Tables/") && url.AbsolutePath.Split('/').Length == 3) { var tableName = url.AbsolutePath.Split('/')[2]; @@ -375,6 +406,18 @@ public bool HandleExpand(ExpandParams request) return true; } + private bool HasEntity(DataSourceWithInfo dataSource, string logicalName) + { + try + { + return dataSource.Metadata[logicalName] != null; + } + catch + { + return false; + } + } + private bool HandleRefresh(RefreshParams args) { return HandleExpand(args); diff --git a/MarkMpn.Sql4Cds.XTB/Autocomplete.cs b/MarkMpn.Sql4Cds.XTB/Autocomplete.cs index 789735e4..3daaac2f 100644 --- a/MarkMpn.Sql4Cds.XTB/Autocomplete.cs +++ b/MarkMpn.Sql4Cds.XTB/Autocomplete.cs @@ -550,6 +550,12 @@ public IEnumerable GetSuggestions(string text, int pos) schemaName == "archive" && (e.IsRetentionEnabled == true || e.IsArchivalEnabled == true) ) + || + ( + schemaName == "bin" && + instance.Metadata.RecycleBinEntities != null && + instance.Metadata.RecycleBinEntities.Contains(e.LogicalName) + ) ) ); @@ -769,6 +775,7 @@ private IEnumerable AutocompleteTableName(string currentWor } else if (TryParseTableName(currentWord, out var instanceName, out var schemaName, out var tableName, out var parts, out var lastPartLength)) { + _dataSources.TryGetValue(instanceName, out var instance); var lastPart = tableName; if (parts == 1) @@ -780,18 +787,18 @@ private IEnumerable AutocompleteTableName(string currentWor if (parts == 1 || (parts == 2 && _dataSources.ContainsKey(schemaName))) { // Could be a schema name - if ("dbo".StartsWith(lastPart, StringComparison.OrdinalIgnoreCase)) - list.Add(new SchemaAutocompleteItem("dbo", lastPartLength)); + var schemaNames = (IEnumerable)new[] { "dbo", "archive", "metadata" }; + if (instance?.Metadata?.RecycleBinEntities != null) + schemaNames = schemaNames.Append("bin"); - if ("archive".StartsWith(lastPart, StringComparison.OrdinalIgnoreCase)) - list.Add(new SchemaAutocompleteItem("archive", lastPartLength)); + schemaNames = schemaNames.Where(s => s.StartsWith(lastPart, StringComparison.OrdinalIgnoreCase)); - if ("metadata".StartsWith(lastPart, StringComparison.OrdinalIgnoreCase)) - list.Add(new SchemaAutocompleteItem("metadata", lastPartLength)); + foreach (var schema in schemaNames) + list.Add(new SchemaAutocompleteItem(schema, lastPartLength)); } // Could be a table name - if (_dataSources.TryGetValue(instanceName, out var instance) && instance.Entities != null) + if (instance?.Entities != null) { IEnumerable entities; IEnumerable messages = Array.Empty(); @@ -817,6 +824,11 @@ private IEnumerable AutocompleteTableName(string currentWor messages = instance.Messages.GetAllMessages(); } } + else if (schemaName.Equals("bin", StringComparison.OrdinalIgnoreCase)) + { + // Suggest tables that are enabled for the recycle bin + entities = instance.Entities.Where(e => instance.Metadata.RecycleBinEntities != null && instance.Metadata.RecycleBinEntities.Contains(e.LogicalName)); + } else { entities = Array.Empty(); @@ -853,6 +865,7 @@ private IEnumerable AutocompleteSprocName(string currentWor } else if (TryParseTableName(currentWord, out var instanceName, out var schemaName, out var tableName, out var parts, out var lastPartLength)) { + _dataSources.TryGetValue(instanceName, out var instance); var lastPart = tableName; if (parts == 1) @@ -864,18 +877,18 @@ private IEnumerable AutocompleteSprocName(string currentWor if (parts == 1 || parts == 2) { // Could be a schema name - if ("dbo".StartsWith(lastPart, StringComparison.OrdinalIgnoreCase)) - list.Add(new SchemaAutocompleteItem("dbo", lastPartLength)); + var schemaNames = (IEnumerable)new[] { "dbo", "archive", "metadata" }; + if (instance?.Metadata?.RecycleBinEntities != null) + schemaNames = schemaNames.Append("bin"); - if ("archive".StartsWith(lastPart, StringComparison.OrdinalIgnoreCase)) - list.Add(new SchemaAutocompleteItem("archive", lastPartLength)); + schemaNames = schemaNames.Where(s => s.StartsWith(lastPart, StringComparison.OrdinalIgnoreCase)); - if ("metadata".StartsWith(lastPart, StringComparison.OrdinalIgnoreCase)) - list.Add(new SchemaAutocompleteItem("metadata", lastPartLength)); + foreach (var schema in schemaNames) + list.Add(new SchemaAutocompleteItem(schema, lastPartLength)); } // Could be a sproc name - if (schemaName.Equals("dbo", StringComparison.OrdinalIgnoreCase) && _dataSources.TryGetValue(instanceName, out var instance) && instance.Messages != null) + if (schemaName.Equals("dbo", StringComparison.OrdinalIgnoreCase) && instance?.Messages != null) list.AddRange(instance.Messages.GetAllMessages().Where(x => x.IsValidAsStoredProcedure()).Select(e => new SprocAutocompleteItem(e, lastPartLength))); } @@ -1480,7 +1493,10 @@ public override string ToolTipTitle public override string ToolTipText { - get => Text == "metadata" ? "Schema containing the metadata information" : "Schema containing the data tables"; + get => Text == "metadata" ? "Schema containing the metadata information" : + Text == "archive" ? "Schema containing long-term retention tables" : + Text == "bin" ? "Schema containing recycle bin tables" : + "Schema containing the data tables"; set => base.ToolTipText = value; } } diff --git a/MarkMpn.Sql4Cds.XTB/FetchXml2SqlSettingsForm.cs b/MarkMpn.Sql4Cds.XTB/FetchXml2SqlSettingsForm.cs index 260bb2db..d45f8fe5 100644 --- a/MarkMpn.Sql4Cds.XTB/FetchXml2SqlSettingsForm.cs +++ b/MarkMpn.Sql4Cds.XTB/FetchXml2SqlSettingsForm.cs @@ -208,6 +208,8 @@ public bool TryGetValue(string logicalName, out EntityMetadata metadata) { return _cache.TryGetValue(logicalName, out metadata); } + + public string[] RecycleBinEntities => throw new NotImplementedException(); } } } diff --git a/MarkMpn.Sql4Cds.XTB/ObjectExplorer.cs b/MarkMpn.Sql4Cds.XTB/ObjectExplorer.cs index acad9aa4..3e28166c 100644 --- a/MarkMpn.Sql4Cds.XTB/ObjectExplorer.cs +++ b/MarkMpn.Sql4Cds.XTB/ObjectExplorer.cs @@ -12,6 +12,8 @@ using Microsoft.Xrm.Sdk.Metadata.Query; using Microsoft.Xrm.Tooling.Connector; using System.Threading.Tasks; +using Microsoft.Xrm.Sdk.Query; +using Microsoft.Xrm.Sdk; namespace MarkMpn.Sql4Cds.XTB { @@ -72,13 +74,25 @@ public IEnumerable GetImages() return imageList.Images.OfType(); } - private TreeNode[] LoadEntities(TreeNode parent, bool archival) + enum EntityType + { + Regular, + Archive, + RecycleBin + } + + private TreeNode[] LoadEntities(TreeNode parent, EntityType entityType) { var connection = GetService(parent); var metadata = EntityCache.GetEntities(connection.MetadataCacheLoader, connection.ServiceClient); + var recycleBinEntities = _dataSources[connection.ConnectionName].Metadata.RecycleBinEntities; return metadata - .Where(e => !archival || e.IsArchivalEnabled == true || e.IsRetentionEnabled == true) + .Where(e => + entityType == EntityType.Regular || + (entityType == EntityType.Archive && (e.IsArchivalEnabled == true || e.IsRetentionEnabled == true)) || + (entityType == EntityType.RecycleBin && recycleBinEntities.Contains(e.LogicalName)) + ) .OrderBy(e => e.LogicalName) .Select(e => { @@ -89,14 +103,28 @@ private TreeNode[] LoadEntities(TreeNode parent, bool archival) SetIcon(attrsNode, "Folder"); AddVirtualChildNodes(attrsNode, LoadAttributes); - if (!archival) + if (entityType != EntityType.Archive) { var relsNode = node.Nodes.Add("Relationships"); SetIcon(relsNode, "Folder"); AddVirtualChildNodes(relsNode, LoadRelationships); } - parent.Tag = archival ? "archive" : "dbo"; + switch (entityType) + { + case EntityType.Regular: + parent.Tag = "dbo"; + break; + + case EntityType.Archive: + parent.Tag = "archive"; + break; + + case EntityType.RecycleBin: + parent.Tag = "bin"; + break; + } + return node; }) .ToArray(); @@ -142,13 +170,20 @@ private void AddConnectionChildNodes(ConnectionDetail con, CrmServiceClient svc, { var entitiesNode = conNode.Nodes.Add("Entities"); SetIcon(entitiesNode, "Folder"); - AddVirtualChildNodes(entitiesNode, parent => LoadEntities(parent, false)); + AddVirtualChildNodes(entitiesNode, parent => LoadEntities(parent, EntityType.Regular)); if (new Uri(con.OrganizationServiceUrl).Host.EndsWith(".dynamics.com")) { var archivalNode = conNode.Nodes.Add("Long Term Retention"); SetIcon(archivalNode, "Folder"); - AddVirtualChildNodes(archivalNode, parent => LoadEntities(parent, true)); + AddVirtualChildNodes(archivalNode, parent => LoadEntities(parent, EntityType.Archive)); + } + + if (_dataSources[con.ConnectionName].Metadata.RecycleBinEntities != null) + { + var recycleBinNode = conNode.Nodes.Add("Recycle Bin"); + SetIcon(recycleBinNode, "Folder"); + AddVirtualChildNodes(recycleBinNode, parent => LoadEntities(parent, EntityType.RecycleBin)); } var metadataNode = conNode.Nodes.Add("Metadata"); diff --git a/MarkMpn.Sql4Cds.XTB/SharedMetadataCache.cs b/MarkMpn.Sql4Cds.XTB/SharedMetadataCache.cs index 3ba43d00..af3a4c68 100644 --- a/MarkMpn.Sql4Cds.XTB/SharedMetadataCache.cs +++ b/MarkMpn.Sql4Cds.XTB/SharedMetadataCache.cs @@ -11,6 +11,9 @@ namespace MarkMpn.Sql4Cds.XTB { + /// + /// An implementation that uses the metadata cache provided by XrmToolBox where possible + /// class SharedMetadataCache : IAttributeMetadataCache { private readonly ConnectionDetail _connection; @@ -28,6 +31,7 @@ public SharedMetadataCache(ConnectionDetail connection, IOrganizationService org _innerCache = new AttributeMetadataCache(org); } + /// public EntityMetadata this[string name] { get @@ -44,6 +48,7 @@ public EntityMetadata this[string name] } } + /// public EntityMetadata this[int otc] { get @@ -60,6 +65,7 @@ public EntityMetadata this[int otc] } } + /// public bool TryGetMinimalData(string logicalName, out EntityMetadata metadata) { if (!CacheReady) @@ -70,6 +76,7 @@ public bool TryGetMinimalData(string logicalName, out EntityMetadata metadata) return _entitiesByName.TryGetValue(logicalName, out metadata); } + /// public bool TryGetValue(string logicalName, out EntityMetadata metadata) { if (!CacheReady) @@ -80,6 +87,9 @@ public bool TryGetValue(string logicalName, out EntityMetadata metadata) return _entitiesByName.TryGetValue(logicalName, out metadata); } + /// + public string[] RecycleBinEntities => _innerCache.RecycleBinEntities; + private bool CacheReady { get