private static void WriteDataSources(DirectoryWriter dir, CanvasDocument app, ErrorContainer errors) { // Data Sources - write out each individual source. HashSet <string> filenames = new HashSet <string>(); foreach (var kvp in app.GetDataSources()) { // Filename doesn't actually matter, but careful to avoid collisions and overwriting. // Also be determinstic. string filename = kvp.Key + ".json"; if (!filenames.Add(filename.ToLower())) { int index = 1; var altFileName = kvp.Key + "_" + index + ".json"; while (!filenames.Add(altFileName.ToLower())) { ++index; } errors.GenericWarning("Data source name collision: " + filename + ", writing as " + altFileName + " to avoid."); filename = altFileName; } var dataSourceStateToWrite = kvp.Value.JsonClone().OrderBy(ds => ds.Name, StringComparer.Ordinal); DataSourceDefinition dataSourceDef = null; // Split out the changeable parts of the data source. foreach (var ds in dataSourceStateToWrite.Where(ds => ds.Type != "ViewInfo")) { // CDS DataSource if (ds.TableDefinition != null) { dataSourceDef = new DataSourceDefinition(); dataSourceDef.TableDefinition = Utility.JsonParse <DataSourceTableDefinition>(ds.TableDefinition); dataSourceDef.DatasetName = ds.DatasetName; dataSourceDef.EntityName = ds.RelatedEntityName ?? ds.Name; ds.DatasetName = null; ds.TableDefinition = null; } // CDP DataSource else if (ds.DataEntityMetadataJson != null) { if (ds.ApiId == "/providers/microsoft.powerapps/apis/shared_commondataservice") { // This is the old CDS connector, we can't support it since it's optionset format is incompatable with the newer one errors.ValidationError($"Connection {ds.Name} is using the old CDS connector which is incompatable with this tool"); throw new DocumentException(); } dataSourceDef = new DataSourceDefinition(); dataSourceDef.DataEntityMetadataJson = ds.DataEntityMetadataJson; dataSourceDef.EntityName = ds.Name; dataSourceDef.TableName = ds.TableName; ds.TableName = null; ds.DataEntityMetadataJson = null; } else if (ds.Type == "OptionSetInfo") { // This looks like a left over from previous versions of studio, account for it by // tracking optionsets with empty dataset names ds.DatasetName = ds.DatasetName == null ? string.Empty : null; } else if (ds.WadlMetadata != null) { // For some reason some connectors have both, investigate if one could be discarded by the server? if (ds.WadlMetadata.WadlXml != null) { dir.WriteAllXML(WadlPackageDir, filename.Replace(".json", ".xml"), ds.WadlMetadata.WadlXml); } if (ds.WadlMetadata.SwaggerJson != null) { dir.WriteAllJson(SwaggerPackageDir, filename, JsonSerializer.Deserialize <SwaggerDefinition>(ds.WadlMetadata.SwaggerJson, Utility._jsonOpts)); } ds.WadlMetadata = null; } } if (dataSourceDef != null) { TrimViewNames(dataSourceStateToWrite, dataSourceDef.DatasetName); } if (dataSourceDef?.DatasetName != null && app._dataSourceReferences.TryGetValue(dataSourceDef.DatasetName, out var referenceJson)) { // copy over the localconnectionreference if (referenceJson.dataSources.TryGetValue(dataSourceDef.EntityName, out var dsRef)) { dataSourceDef.LocalReferenceDSJson = dsRef; } dataSourceDef.InstanceUrl = referenceJson.instanceUrl; dataSourceDef.ExtensionData = referenceJson.ExtensionData; } if (dataSourceDef != null) { dir.WriteAllJson(DataSourcePackageDir, filename, dataSourceDef); } dir.WriteAllJson(DataSourcesDir, filename, dataSourceStateToWrite); } }
// Get everything that should be stored as a file in the .msapp. private static IEnumerable <FileEntry> GetMsAppFiles(this CanvasDocument app, ErrorContainer errors) { // Loose files foreach (var file in app._unknownFiles.Values) { yield return(file); } yield return(ToFile(FileKind.Themes, app._themes)); var header = app._header.JsonClone(); header.LastSavedDateTimeUTC = app._entropy.GetHeaderLastSaved(); yield return(ToFile(FileKind.Header, header)); var props = app._properties.JsonClone(); if (app._connections != null) { var json = Utilities.JsonSerialize(app._connections); props.LocalConnectionReferences = json; } if (app._dataSourceReferences != null) { var json = Utilities.JsonSerialize(app._dataSourceReferences); // Some formats serialize empty as "", some serialize as "{}" if (app._dataSourceReferences.Count == 0) { if (app._entropy.LocalDatabaseReferencesAsEmpty) { json = ""; } else { json = "{}"; } } props.LocalDatabaseReferences = json; } if (app._libraryReferences != null) { var json = Utilities.JsonSerialize(app._libraryReferences); props.LibraryDependencies = json; } yield return(ToFile(FileKind.Properties, props)); var(publishInfo, logoFile) = app.TransformLogoOnSave(); yield return(logoFile); if (publishInfo != null) { yield return(ToFile(FileKind.PublishInfo, publishInfo)); } // "DataComponent" data sources are not part of DataSource.json, and instead in their own file var dataSources = new DataSourcesJson { DataSources = app.GetDataSources() .SelectMany(x => x.Value) .Where(x => !x.IsDataComponent) .OrderBy(x => app._entropy.GetOrder(x)) .ToArray() }; yield return(ToFile(FileKind.DataSources, dataSources)); var sourceFiles = new List <SourceFile>(); var idRestorer = new UniqueIdRestorer(app._entropy); var maxPublishOrderIndex = app._entropy.PublishOrderIndices.Any() ? app._entropy.PublishOrderIndices.Values.Max() : 0; // Rehydrate sources before yielding any to be written, processing component defs first foreach (var controlData in app._screens.Concat(app._components) .OrderBy(source => (app._editorStateStore.TryGetControlState(source.Value.Name.Identifier, out var control) && (control.IsComponentDefinition ?? false)) ? -1 : 1)) { var sourceFile = IRStateHelpers.CombineIRAndState(controlData.Value, errors, app._editorStateStore, app._templateStore, idRestorer, app._entropy); // Offset the publishOrderIndex based on Entropy.json foreach (var ctrl in sourceFile.Flatten()) { if (app._entropy.PublishOrderIndices.TryGetValue(ctrl.Name, out var index)) { ctrl.PublishOrderIndex = index; } else { ctrl.PublishOrderIndex = ++maxPublishOrderIndex; } } sourceFiles.Add(sourceFile); } CheckUniqueIds(errors, sourceFiles); // Repair order when screens are unchanged if (sourceFiles.Where(file => !ExcludeControlFromScreenOrdering(file)).Count() == app._screenOrder.Count && sourceFiles.Where(file => !ExcludeControlFromScreenOrdering(file)).All(file => app._screenOrder.Contains(file.ControlName))) { double i = 0.0; foreach (var screen in app._screenOrder) { sourceFiles.First(file => file.ControlName == screen).Value.TopParent.Index = i; i += 1; } } else { // Make up an order, it doesn't really matter. double i = 0.0; foreach (var sourceFile in sourceFiles) { if (ExcludeControlFromScreenOrdering(sourceFile)) { continue; } sourceFile.Value.TopParent.Index = i; i += 1; } } RepairComponentInstanceIndex(app._entropy?.ComponentIndexes ?? new Dictionary <string, double>(), sourceFiles); // This ordering is essential, we need to match the order in which Studio writes the files to replicate certain order-dependent behavior. foreach (var sourceFile in sourceFiles.OrderBy(file => file.GetMsAppFilename())) { yield return(sourceFile.ToMsAppFile()); } var componentTemplates = new List <TemplateMetadataJson>(); foreach (var template in app._templateStore.Contents.Where(template => template.Value.IsComponentTemplate ?? false)) { if (((template.Value.CustomProperties?.Any() ?? false) || template.Value.ComponentAllowCustomization.HasValue) && !(template.Value.ComponentManifest?.IsDataComponent ?? false)) { componentTemplates.Add(template.Value.ToTemplateMetadata(app._entropy)); } } app._templates = new TemplatesJson() { ComponentTemplates = componentTemplates.Any() ? componentTemplates.OrderBy(x => app._entropy.GetComponentOrder(x)).ToArray() : null, UsedTemplates = app._templates.UsedTemplates.OrderBy(x => app._entropy.GetOrder(x)).ToArray() }; yield return(ToFile(FileKind.Templates, app._templates)); var componentsMetadata = new List <ComponentsMetadataJson.Entry>(); var dctemplate = new List <TemplateMetadataJson>(); foreach (var componentTemplate in app._templateStore.Contents.Values.Where(state => state.ComponentManifest != null)) { var manifest = componentTemplate.ComponentManifest; componentsMetadata.Add(new ComponentsMetadataJson.Entry { Name = manifest.Name, TemplateName = manifest.TemplateGuid, ExtensionData = manifest.ExtensionData }); if (manifest.IsDataComponent) { var controlId = GetDataComponentDefinition(sourceFiles.Select(source => source.Value), manifest.TemplateGuid, errors).ControlUniqueId; var template = new TemplateMetadataJson { Name = manifest.TemplateGuid, ComponentType = ComponentType.DataComponent, Version = app._entropy.GetTemplateVersion(manifest.TemplateGuid), IsComponentLocked = false, ComponentChangedSinceFileImport = true, ComponentAllowCustomization = true, CustomProperties = componentTemplate.CustomProperties, DataComponentDefinitionKey = manifest.DataComponentDefinitionKey }; // Rehydrate fields. template.DataComponentDefinitionKey.ControlUniqueId = controlId; dctemplate.Add(template); } } if (componentsMetadata.Count > 0) { // If the components file is present, then write out all files. yield return(ToFile(FileKind.ComponentsMetadata, new ComponentsMetadataJson { Components = componentsMetadata .OrderBy(x => app._entropy.GetOrder(x)) .ToArray() })); } if (dctemplate.Count > 0) { yield return(ToFile(FileKind.DataComponentTemplates, new DataComponentTemplatesJson { ComponentTemplates = dctemplate .OrderBy(x => app._entropy.GetOrder(x)) .ToArray() })); } // Rehydrate the DataComponent DataSource file. { IEnumerable <DataComponentSourcesJson.Entry> ds = from item in app.GetDataSources().SelectMany(x => x.Value).Where(x => x.IsDataComponent) select item.DataComponentDetails; var dsArray = ds.ToArray(); // backcompat-nit: if we have any DC, then always emit the DC Sources file, even if empty. // if (dcmetadataList.Count > 0) if (dctemplate.Count > 0 || dsArray.Length > 0) { yield return(ToFile(FileKind.DataComponentSources, new DataComponentSourcesJson { DataSources = dsArray })); } } if (app._appCheckerResultJson != null) { yield return(ToFile(FileKind.AppCheckerResult, app._appCheckerResultJson)); } if (app._resourcesJson != null) { var resources = app._resourcesJson.JsonClone(); foreach (var resource in resources.Resources) { if (resource.ResourceKind == "LocalFile") { var rootPath = string.Empty; if (app._entropy?.LocalResourceRootPaths.TryGetValue(resource.Name, out rootPath) ?? false) { resource.RootPath = rootPath; } else { resource.RootPath = string.Empty; } } } yield return(ToFile(FileKind.Resources, resources)); } foreach (var assetFile in app._assetFiles) { yield return(new FileEntry { Name = FilePath.RootedAt("Assets", assetFile.Value.Name), RawBytes = assetFile.Value.RawBytes }); } }