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);
            }
        }
Example #2
0
        // 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
                });
            }
        }