├── .github └── workflows │ └── build.yaml ├── .gitignore ├── APS-PowerBi-DevCon2024.pdf ├── LICENSE ├── README.md ├── connectors ├── DesignPropsConnector │ ├── .gitignore │ ├── .vscode │ │ └── settings.json │ ├── DataManagement.pqm │ ├── DesignPropsConnector.pq │ ├── DesignPropsConnector.proj │ ├── DesignPropsConnector.query.pq │ ├── LICENSE │ ├── ModelDerivative.pqm │ ├── README.md │ ├── docs │ │ └── set-credential.png │ ├── icons │ │ ├── DesignPropsConnector16.png │ │ ├── DesignPropsConnector20.png │ │ ├── DesignPropsConnector24.png │ │ ├── DesignPropsConnector32.png │ │ ├── DesignPropsConnector40.png │ │ ├── DesignPropsConnector48.png │ │ ├── DesignPropsConnector64.png │ │ └── DesignPropsConnector80.png │ └── resources.resx ├── PremiumReportingConnector │ ├── .gitignore │ ├── .vscode │ │ └── settings.json │ ├── LICENSE │ ├── PremiumReporting.pqm │ ├── PremiumReportingConnector.pq │ ├── PremiumReportingConnector.proj │ ├── PremiumReportingConnector.query.pq │ ├── README.md │ ├── docs │ │ └── set-credential.png │ ├── icons │ │ ├── PremiumReportingConnector16.png │ │ ├── PremiumReportingConnector20.png │ │ ├── PremiumReportingConnector24.png │ │ ├── PremiumReportingConnector32.png │ │ ├── PremiumReportingConnector40.png │ │ ├── PremiumReportingConnector48.png │ │ ├── PremiumReportingConnector64.png │ │ └── PremiumReportingConnector80.png │ └── resources.resx ├── Shared │ ├── Diagnostics.pqm │ ├── NavigationTable.pqm │ ├── OAuth.pqm │ └── OAuthPKCE.pqm └── TokenFlexConnector │ ├── .gitignore │ ├── .vscode │ └── settings.json │ ├── LICENSE │ ├── README.md │ ├── TokenFlex.pqm │ ├── TokenFlexConnector.pq │ ├── TokenFlexConnector.proj │ ├── TokenFlexConnector.query.pq │ ├── docs │ └── set-credential.png │ ├── icons │ ├── TokenFlexConnector16.png │ ├── TokenFlexConnector20.png │ ├── TokenFlexConnector24.png │ ├── TokenFlexConnector32.png │ ├── TokenFlexConnector40.png │ ├── TokenFlexConnector48.png │ ├── TokenFlexConnector64.png │ └── TokenFlexConnector80.png │ └── resources.resx ├── screenshot.png ├── services ├── aps-shares-app │ ├── .gitignore │ ├── .vscode │ │ └── launch.json │ ├── LICENSE │ ├── README.md │ ├── config.js │ ├── index.js │ ├── package-lock.json │ ├── package.json │ ├── routes │ │ ├── auth.js │ │ ├── shares.js │ │ └── token.js │ ├── screenshot.png │ ├── shares.js │ └── views │ │ ├── error.ejs │ │ └── index.ejs └── ssa-auth-app │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── lib │ └── auth.js │ ├── package-lock.json │ ├── package.json │ ├── server.js │ └── tools │ └── create-service-account.js └── visuals ├── aps-viewer-visual ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .vscode │ ├── launch.json │ └── settings.json ├── LICENSE ├── README.md ├── assets │ └── icon.png ├── capabilities.json ├── docs │ ├── add-developer-visual.png │ ├── add-element-ids.png │ └── add-token-endpoint.png ├── package-lock.json ├── package.json ├── pbiviz.json ├── src │ ├── settings.ts │ ├── viewer.utils.ts │ └── visual.ts ├── style │ └── visual.less ├── tsconfig.json └── tslint.json └── tandem-viewer-visual └── release └── tandem-visual-visual.1.0.0.1.pbiviz /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | name: Build & Release 9 | 10 | jobs: 11 | aps-viewer-visual: 12 | name: Build Viewer Visual 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout Code 16 | uses: actions/checkout@v3 17 | - name: Setup Node.js 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: 20 21 | - name: Install dependencies 22 | working-directory: ./visuals/aps-viewer-visual 23 | run: npm ci && npm install -g powerbi-visuals-tools 24 | - name: Build 25 | working-directory: ./visuals/aps-viewer-visual 26 | run: npm run package 27 | - name: Upload Artifact 28 | uses: actions/upload-artifact@v3 29 | with: 30 | name: aps-viewer-visual 31 | path: ./visuals/aps-viewer-visual/dist/*.pbiviz 32 | create-release: 33 | name: Create New Release 34 | permissions: 35 | contents: write 36 | runs-on: ubuntu-latest 37 | needs: [aps-viewer-visual] 38 | steps: 39 | - name: Checkout Code 40 | uses: actions/checkout@v3 41 | - name: Download Artifacts 42 | uses: actions/download-artifact@v3 43 | - name: Create Release 44 | uses: Roang-zero1/github-create-release-action@v3 45 | env: 46 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 47 | - name: Upload Release Artifacts 48 | uses: Roang-zero1/github-upload-release-artifacts-action@v3 49 | with: 50 | args: "aps-viewer-visual/" 51 | env: 52 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | Thumbs.db -------------------------------------------------------------------------------- /APS-PowerBi-DevCon2024.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autodesk-platform-services/aps-powerbi-tools/3bc6aeb22e20344ba0c865829bd5af8bd39aba8c/APS-PowerBi-DevCon2024.pdf -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 Autodesk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # APS PowerBI Tools 2 | 3 | Collection of tools for accessing [Autodesk Platform Services](https://aps.autodesk.com) design data - 2D/3D views as well as element properties - from [Power BI](https://powerbi.com) reports. 4 | 5 | ![Screenshot](./screenshot.png) 6 | 7 | > For more information about the history of this project, check out the [slides from APS DevCon 2024](./APS-PowerBi-DevCon2024.pdf). 8 | 9 | There are many different ways in which report authors might want to access design data in APS, for example: 10 | 11 | - displaying designs in a viewer - some report authors may want to limit access to specific users only, while others may want the designs to be viewable by everyone 12 | - importing design properties - some report authors may want to access the design properties directly from ACC, while others may already have the data available in a different form (for example, as an [Excel spreadsheet](https://github.com/autodesk-platform-services/aps-extract-spreadsheet)) 13 | 14 | That is why we provide a collection of custom data connectors, visuals, and supporting web services for _developers_ and _power users_ who can choose just the components that they need, customize them, and use them in their own Power BI reports. For more information, see the README file of each individual component. 15 | 16 | ## Troubleshooting 17 | 18 | Please contact us via https://aps.autodesk.com/get-help. 19 | 20 | ## License 21 | 22 | This sample is licensed under the terms of the [MIT License](http://opensource.org/licenses/MIT). Please see the [LICENSE](LICENSE) file for more details. 23 | -------------------------------------------------------------------------------- /connectors/DesignPropsConnector/.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | obj 3 | secrets.json 4 | config.json 5 | .DS_Store 6 | Thumbs.db 7 | -------------------------------------------------------------------------------- /connectors/DesignPropsConnector/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "powerquery.sdk.defaultQueryFile": "${workspaceFolder}\\${workspaceFolderBasename}.query.pq", 3 | "powerquery.sdk.defaultExtension": "${workspaceFolder}\\bin\\AnyCPU\\Debug\\${workspaceFolderBasename}.mez", 4 | "powerquery.general.mode": "SDK" 5 | } 6 | -------------------------------------------------------------------------------- /connectors/DesignPropsConnector/DataManagement.pqm: -------------------------------------------------------------------------------- 1 | let 2 | Get = (url as text) as record => 3 | let 4 | response = Web.Contents(url) 5 | in 6 | Json.Document(response), 7 | 8 | GetHubs = () as list => 9 | let 10 | json = Get("https://developer.api.autodesk.com/project/v1/hubs"), 11 | hubs = json[data] 12 | in 13 | hubs, 14 | 15 | GetProjects = (hubId as text) as list => 16 | let 17 | GetPage = (pageNumber as number) as record => Get("https://developer.api.autodesk.com/project/v1/hubs/" & hubId & "/projects?page[number]=" & Number.ToText(pageNumber)), 18 | Collate = (pageNumber as number, accumulatedList as list) as list => 19 | let 20 | result = GetPage(pageNumber), 21 | projects = result[data], 22 | newAccumulatedList = List.Combine({accumulatedList, projects}) 23 | in 24 | if List.Count(projects) > 0 then @Collate(pageNumber + 1, newAccumulatedList) else newAccumulatedList, 25 | projects = Collate(0, {}) 26 | in 27 | projects, 28 | 29 | GetTopFolders = (hubId as text, projectId as text) as list => 30 | let 31 | json = Get("https://developer.api.autodesk.com/project/v1/hubs/" & hubId & "/projects/" & projectId & "/topFolders"), 32 | folders = json[data] 33 | in 34 | folders, 35 | 36 | GetFolderContents = (projectId as text, folderId as text) as list => 37 | let 38 | GetPage = (pageNumber as number) as record => Get("https://developer.api.autodesk.com/data/v1/projects/" & projectId & "/folders/" & folderId & "/contents?page[number]=" & Number.ToText(pageNumber)), 39 | Collate = (pageNumber as number, accumulatedList as list) as list => 40 | let 41 | result = GetPage(pageNumber), 42 | contents = result[data], 43 | newAccumulatedList = List.Combine({accumulatedList, contents}) 44 | in 45 | if List.Count(contents) > 0 then @Collate(pageNumber + 1, newAccumulatedList) else newAccumulatedList, 46 | contents = Collate(0, {}) 47 | in 48 | contents, 49 | 50 | GetItemVersions = (projectId as text, itemId as text) as list => 51 | let 52 | GetPage = (pageNumber as number) as record => Get("https://developer.api.autodesk.com/data/v1/projects/" & projectId & "/items/" & itemId & "/versions?page[number]=" & Number.ToText(pageNumber)), 53 | Collate = (pageNumber as number, accumulatedList as list) as list => 54 | let 55 | result = GetPage(pageNumber), 56 | versions = result[data], 57 | newAccumulatedList = List.Combine({accumulatedList, versions}) 58 | in 59 | if List.Count(versions) > 0 then @Collate(pageNumber + 1, newAccumulatedList) else newAccumulatedList, 60 | versions = Collate(0, {}) 61 | in 62 | versions, 63 | 64 | GetVersionDetails = (projectId as text, versionId as text) as record => 65 | let 66 | json = Get("https://developer.api.autodesk.com/data/v1/projects/" & projectId & "/versions/" & Uri.EscapeDataString(versionId)), 67 | details = json[data] 68 | in 69 | details 70 | in 71 | [ 72 | GetHubs = GetHubs, 73 | GetProjects = GetProjects, 74 | GetTopFolders = GetTopFolders, 75 | GetFolderContents = GetFolderContents, 76 | GetItemVersions = GetItemVersions, 77 | GetVersionDetails = GetVersionDetails 78 | ] -------------------------------------------------------------------------------- /connectors/DesignPropsConnector/DesignPropsConnector.pq: -------------------------------------------------------------------------------- 1 | [Version = "0.0.1"] 2 | section DesignPropsConnector; 3 | 4 | DesignPropsConnector = [ 5 | Authentication = [ 6 | OAuth = [ 7 | StartLogin = OAuth[StartLogin], 8 | FinishLogin = OAuth[FinishLogin], 9 | Refresh = OAuth[RefreshToken] 10 | ] 11 | ] 12 | ]; 13 | 14 | DesignPropsConnector.Publish = [ 15 | Beta = true, 16 | Category = "Other", 17 | ButtonText = {Extension.LoadString("ButtonTitle"), Extension.LoadString("ButtonHelp")}, 18 | LearnMoreUrl = "https://github.com/autodesk-platform-services/aps-powerbi-tools", 19 | SourceImage = DesignPropsConnector.Icons, 20 | SourceTypeImage = DesignPropsConnector.Icons 21 | ]; 22 | 23 | DesignPropsConnector.Icons = [ 24 | Icon16 = { Extension.Contents("DesignPropsConnector16.png"), Extension.Contents("DesignPropsConnector20.png"), Extension.Contents("DesignPropsConnector24.png"), Extension.Contents("DesignPropsConnector32.png") }, 25 | Icon32 = { Extension.Contents("DesignPropsConnector32.png"), Extension.Contents("DesignPropsConnector40.png"), Extension.Contents("DesignPropsConnector48.png"), Extension.Contents("DesignPropsConnector64.png") } 26 | ]; 27 | 28 | [DataSource.Kind = "DesignPropsConnector", Publish = "DesignPropsConnector.Publish"] 29 | shared DesignPropsConnector.Contents = () => GetHubsNavigationTable(); 30 | 31 | GetHubsNavigationTable = () as table => 32 | let 33 | hubs = DataManagement[GetHubs](), 34 | treeNodes = List.Transform(hubs, each [ 35 | Name = _[attributes][name], 36 | Key = _[id], 37 | Data = GetProjectsNavigationTable(_[id]), 38 | ItemKind = "DatabaseServer", 39 | ItemName = "Hub", 40 | IsLeaf = false 41 | ]) 42 | in 43 | NavigationTable.FromRecords(treeNodes); 44 | 45 | GetProjectsNavigationTable = (hubId as text) as table => 46 | let 47 | projects = DataManagement[GetProjects](hubId), 48 | treeNodes = List.Transform(projects, each [ 49 | Name = _[attributes][name], 50 | Key = _[id], 51 | Data = GetTopFoldersNavigationTable(hubId, _[id]), 52 | ItemKind = "Database", 53 | ItemName = "Project", 54 | IsLeaf = false 55 | ]) 56 | in 57 | NavigationTable.FromRecords(treeNodes); 58 | 59 | GetTopFoldersNavigationTable = (hubId as text, projectId as text) as table => 60 | let 61 | folders = DataManagement[GetTopFolders](hubId, projectId), 62 | treeNodes = List.Transform(folders, each [ 63 | Name = _[attributes][displayName], 64 | Key = _[id], 65 | Data = GetFolderContentsNavigationTable(projectId, _[id]), 66 | ItemKind = "Folder", 67 | ItemName = "Folder", 68 | IsLeaf = false 69 | ]) 70 | in 71 | NavigationTable.FromRecords(treeNodes); 72 | 73 | GetFolderContentsNavigationTable = (projectId as text, folderId as text) as table => 74 | let 75 | contents = DataManagement[GetFolderContents](projectId, folderId), 76 | treeNodes = List.Transform(contents, each [ 77 | Name = _[attributes][displayName], 78 | Key = _[id], 79 | Data = if _[type] = "folders" then @GetFolderContentsNavigationTable(projectId, _[id]) else GetItemVersionsNavigationTable(projectId, _[id]), 80 | ItemKind = if _[type] = "folders" then "Folder" else "Sheet", 81 | ItemName = if _[type] = "folders" then "Folder" else "Document", 82 | IsLeaf = false 83 | ]) 84 | in 85 | NavigationTable.FromRecords(treeNodes); 86 | 87 | GetItemVersionsNavigationTable = (projectId as text, itemId as text) as table => 88 | let 89 | versions = DataManagement[GetItemVersions](projectId, itemId), 90 | treeNodes = List.Transform(versions, each [ 91 | Name = Text.Format("Version #[versionNumber] (#[createTime])"), 92 | Key = _[id], 93 | Data = GetDesignProperties(projectId, _[id]), 94 | ItemKind = "Table", 95 | ItemName = "Version", 96 | IsLeaf = true 97 | ]) 98 | in 99 | NavigationTable.FromRecords(treeNodes); 100 | 101 | GetDesignProperties = (projectId as text, versionId as text, optional region as text) as table => 102 | let 103 | version = DataManagement[GetVersionDetails](projectId, versionId), 104 | urn = version[relationships][derivatives][data][id], 105 | views = ModelDerivative[GetModelViews](urn, region), // Get the list of all viewables available for the input URN 106 | firstView = List.First(views), // Get the first viewable 107 | objectTree = ModelDerivative[GetModelTree](urn, firstView[guid], region), // Get the hierarchy of objects in the viewable 108 | flattenedObjectTree = FlattenObjectTree(objectTree), // Flatten the object hierarchy into a table 109 | objectIdPages = List.Split(Table.Column(flattenedObjectTree, "objectid"), 512), // Split object IDs into pages of predefined size 110 | propertyPages = List.Transform(objectIdPages, each ModelDerivative[GetModelProperties](urn, firstView[guid], _, region)), // Retrieve properties for each page 111 | properties = Table.FromRecords(List.Union(propertyPages)), // Combine paged results into a single table 112 | joinedTables = Table.Join(properties, "objectid", flattenedObjectTree, "objectid"), // Join flattened tree and properties tables 113 | selectedColumns = Table.SelectColumns(joinedTables, {"objectid", "externalId", "hierarchy", "name", "is_group", "properties"}), 114 | renamedColumns = Table.RenameColumns(selectedColumns, { 115 | {"objectid", "Object ID"}, 116 | {"externalId", "External ID"}, 117 | {"name", "Name"}, 118 | {"hierarchy", "Hierarchy"}, 119 | {"is_group", "Is Group"}, 120 | {"properties", "Properties"} 121 | }) 122 | in 123 | Table.AddColumn(renamedColumns, "URN", each urn); 124 | 125 | FlattenObjectTree = (tree as record) as table => 126 | let 127 | FlattenNode = (node as record, hierarchy as list) => 128 | if Record.HasFields(node, "objects") then 129 | let entry = [ objectid = node[objectid], hierarchy = hierarchy, is_group = true ] 130 | in {entry} & List.Combine(List.Transform(node[objects], each @FlattenNode(_, hierarchy & {node[name]}))) 131 | else 132 | let entry = [ objectid = node[objectid], hierarchy = hierarchy, is_group = false ] 133 | in {entry}, 134 | flattened = FlattenNode(tree, {}) 135 | in 136 | Table.FromRecords(flattened); 137 | 138 | // 139 | // Load common library functions 140 | // 141 | // TEMPORARY WORKAROUND until we're able to reference other M modules 142 | Extension.LoadFunction = (name as text) => 143 | let 144 | binary = Extension.Contents(name), asText = Text.FromBinary(binary) 145 | in 146 | Expression.Evaluate(asText, #shared); 147 | 148 | NavigationTable = Extension.LoadFunction("NavigationTable.pqm"); 149 | NavigationTable.FromRecords = NavigationTable[FromRecords]; 150 | OAuth = Extension.LoadFunction("OAuthPKCE.pqm"); 151 | DataManagement = Extension.LoadFunction("DataManagement.pqm"); 152 | ModelDerivative = Extension.LoadFunction("ModelDerivative.pqm"); -------------------------------------------------------------------------------- /connectors/DesignPropsConnector/DesignPropsConnector.proj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(MSBuildProjectDirectory)\bin\AnyCPU\Debug\ 5 | $(MSBuildProjectDirectory)\obj\ 6 | $(IntermediateOutputPath)MEZ\ 7 | $(OutputPath)$(MsBuildProjectName).mez 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /connectors/DesignPropsConnector/DesignPropsConnector.query.pq: -------------------------------------------------------------------------------- 1 | let 2 | contents = DesignPropsConnector.Contents() 3 | in 4 | contents 5 | -------------------------------------------------------------------------------- /connectors/DesignPropsConnector/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 Autodesk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /connectors/DesignPropsConnector/ModelDerivative.pqm: -------------------------------------------------------------------------------- 1 | let 2 | GetModelViews = (urn as text, optional region as text) as list => 3 | let 4 | url = "https://developer.api.autodesk.com/" & (if region = "EMEA" then "modelderivative/v2/regions/eu" else "modelderivative/v2") & "/designdata/" & urn & "/metadata", 5 | response = Web.Contents(url), 6 | json = Json.Document(response) 7 | in 8 | json[data][metadata], 9 | 10 | GetModelTree = (urn as text, guid as text, optional region as text) as record => 11 | let 12 | url = "https://developer.api.autodesk.com/" & (if region = "EMEA" then "modelderivative/v2/regions/eu" else "modelderivative/v2") & "/designdata/" & urn & "/metadata/" & guid, 13 | response = Web.Contents(url, [ ManualStatusHandling = {202}, IsRetry = true ]), 14 | metadata = Value.Metadata(response), 15 | json = 16 | if metadata[Response.Status] = 202 then // TODO: retry 17 | error "Could not retrieve object tree. Request is being processed. Please try again later." 18 | else 19 | Json.Document(response) 20 | in 21 | json[data][objects]{0}, 22 | 23 | GetModelProperties = (urn as text, guid as text, objectIds as list, optional region as text) as list => 24 | let 25 | url = "https://developer.api.autodesk.com/" & (if region = "EMEA" then "modelderivative/v2/regions/eu" else "modelderivative/v2") & "/designdata/" & urn & "/metadata/" & guid & "/properties:query", 26 | headers = [#"Content-Type" = "application/json"], 27 | payload = Json.FromValue([ 28 | pagination = [ 29 | limit = List.Count(objectIds) 30 | ], 31 | query = [ 32 | #"$in" = { "objectid" } & objectIds 33 | ] 34 | ]), 35 | response = Web.Contents(url, [ ManualStatusHandling = {202}, IsRetry = true, Headers = headers, Content = payload ]), 36 | metadata = Value.Metadata(response), 37 | json = 38 | if metadata[Response.Status] = 202 then // TODO: retry 39 | error "Could not retrieve properties. Request is being processed. Please try again later." 40 | else 41 | Json.Document(response) 42 | in 43 | json[data][collection] 44 | in 45 | [ 46 | GetModelViews = GetModelViews, 47 | GetModelTree = GetModelTree, 48 | GetModelProperties = GetModelProperties 49 | ] -------------------------------------------------------------------------------- /connectors/DesignPropsConnector/README.md: -------------------------------------------------------------------------------- 1 | # APS Design Properties Connector 2 | 3 | Example [custom Power BI data connector](https://learn.microsoft.com/en-us/power-bi/connect-data/desktop-connector-extensibility) for accessing properties of designs in [Autodesk Platform Services](https://aps.autodesk.com) using [Model Derivative API](https://aps.autodesk.com/en/docs/model-derivative/v2/developers_guide/overview/). 4 | 5 | ## Usage 6 | 7 | See [Design Properties Connector Usage](https://github.com/autodesk-platform-services/aps-powerbi-tools/wiki/Design-Properties-Connector-Usage). 8 | 9 | ## Development 10 | 11 | ### Prerequisites 12 | 13 | - [APS application](https://aps.autodesk.com/en/docs/oauth/v2/tutorials/create-app) of type _Desktop, Mobile, Single-Page App_ (this project uses [PKCE authentication](https://aps.autodesk.com/en/docs/oauth/v2/developers_guide/App-types/native/)) 14 | - [Provision access to ACC or BIM360](https://tutorials.autodesk.io/#provision-access-in-other-products) 15 | - [.NET 8](https://dotnet.microsoft.com/en-us/download/dotnet/8.0) 16 | 17 | ### Building 18 | 19 | - Create a _config.json_ file in the project folder, and populate it with your APS application client ID: 20 | 21 | ```json 22 | { 23 | "APS_CLIENT_ID": "" 24 | } 25 | ``` 26 | 27 | - Register the following Callback URL to your APS application: 28 | 29 | ``` 30 | https://oauth.powerbi.com/views/oauthredirect.html 31 | ``` 32 | 33 | - Build the connector *.mez file (using bash or PowerShell) 34 | 35 | ```bash 36 | dotnet build 37 | ``` 38 | 39 | ### Deploying 40 | 41 | - Copy the generated *.mez file from the _bin/AnyCPU/Debug_ subfolder into Power BI Desktop application as explained [here](https://learn.microsoft.com/en-us/power-bi/connect-data/desktop-connector-extensibility#custom-connectors) 42 | - When selecting data sources in Power BI Desktop, the custom connector will be available under _Other > APS Design Properties Connector (Beta) (Custom)_ 43 | 44 | ### Testing (Visual Studio Code) 45 | 46 | - Make sure you have the [Power Query SDK](https://learn.microsoft.com/en-us/power-query/install-sdk) extension installed 47 | 48 | ![Set credential](./docs/set-credential.png) 49 | 50 | - Create new credentials by clicking the _Set credential_ option (you will be prompted to log in with your Autodesk account) 51 | - Open the [DesignPropsConnector.query.pq](./DesignPropsConnector.query.pq) file 52 | - Run the test query by clicking the _Evaluate current file_ option 53 | 54 | ## License 55 | 56 | This sample is licensed under the terms of the [MIT License](http://opensource.org/licenses/MIT). Please see the [LICENSE](LICENSE) file for more details. 57 | -------------------------------------------------------------------------------- /connectors/DesignPropsConnector/docs/set-credential.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autodesk-platform-services/aps-powerbi-tools/3bc6aeb22e20344ba0c865829bd5af8bd39aba8c/connectors/DesignPropsConnector/docs/set-credential.png -------------------------------------------------------------------------------- /connectors/DesignPropsConnector/icons/DesignPropsConnector16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autodesk-platform-services/aps-powerbi-tools/3bc6aeb22e20344ba0c865829bd5af8bd39aba8c/connectors/DesignPropsConnector/icons/DesignPropsConnector16.png -------------------------------------------------------------------------------- /connectors/DesignPropsConnector/icons/DesignPropsConnector20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autodesk-platform-services/aps-powerbi-tools/3bc6aeb22e20344ba0c865829bd5af8bd39aba8c/connectors/DesignPropsConnector/icons/DesignPropsConnector20.png -------------------------------------------------------------------------------- /connectors/DesignPropsConnector/icons/DesignPropsConnector24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autodesk-platform-services/aps-powerbi-tools/3bc6aeb22e20344ba0c865829bd5af8bd39aba8c/connectors/DesignPropsConnector/icons/DesignPropsConnector24.png -------------------------------------------------------------------------------- /connectors/DesignPropsConnector/icons/DesignPropsConnector32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autodesk-platform-services/aps-powerbi-tools/3bc6aeb22e20344ba0c865829bd5af8bd39aba8c/connectors/DesignPropsConnector/icons/DesignPropsConnector32.png -------------------------------------------------------------------------------- /connectors/DesignPropsConnector/icons/DesignPropsConnector40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autodesk-platform-services/aps-powerbi-tools/3bc6aeb22e20344ba0c865829bd5af8bd39aba8c/connectors/DesignPropsConnector/icons/DesignPropsConnector40.png -------------------------------------------------------------------------------- /connectors/DesignPropsConnector/icons/DesignPropsConnector48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autodesk-platform-services/aps-powerbi-tools/3bc6aeb22e20344ba0c865829bd5af8bd39aba8c/connectors/DesignPropsConnector/icons/DesignPropsConnector48.png -------------------------------------------------------------------------------- /connectors/DesignPropsConnector/icons/DesignPropsConnector64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autodesk-platform-services/aps-powerbi-tools/3bc6aeb22e20344ba0c865829bd5af8bd39aba8c/connectors/DesignPropsConnector/icons/DesignPropsConnector64.png -------------------------------------------------------------------------------- /connectors/DesignPropsConnector/icons/DesignPropsConnector80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autodesk-platform-services/aps-powerbi-tools/3bc6aeb22e20344ba0c865829bd5af8bd39aba8c/connectors/DesignPropsConnector/icons/DesignPropsConnector80.png -------------------------------------------------------------------------------- /connectors/DesignPropsConnector/resources.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | Connect to Design Properties in Autodesk Platform Services. 122 | 123 | 124 | APS Design Properties Connector 125 | 126 | 127 | -------------------------------------------------------------------------------- /connectors/PremiumReportingConnector/.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | obj 3 | secrets.json 4 | config.json 5 | .DS_Store 6 | Thumbs.db 7 | -------------------------------------------------------------------------------- /connectors/PremiumReportingConnector/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "powerquery.sdk.defaultQueryFile": "${workspaceFolder}\\${workspaceFolderBasename}.query.pq", 3 | "powerquery.sdk.defaultExtension": "${workspaceFolder}\\bin\\AnyCPU\\Debug\\${workspaceFolderBasename}.mez", 4 | "powerquery.general.mode": "SDK" 5 | } 6 | -------------------------------------------------------------------------------- /connectors/PremiumReportingConnector/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 Autodesk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /connectors/PremiumReportingConnector/PremiumReporting.pqm: -------------------------------------------------------------------------------- 1 | let 2 | GetContexts = (baseUrl as text) => 3 | let 4 | response = Web.Contents(baseUrl & "/contexts") 5 | in 6 | Json.Document(response), 7 | 8 | RunUsageQuery = (_contextId as text, _fields as list, _metrics as list, baseUrl as text, optional options as record) as record => 9 | let 10 | _options = options ?? [], 11 | headers = [#"Content-Type" = "application/json"], 12 | payload = Json.FromValue([ 13 | fields = _fields, 14 | metrics = _metrics, 15 | where = _options[where] ?? "", 16 | // orderBy = _options[orderBy] ?? "", 17 | contextId = _contextId 18 | ]), 19 | response = Web.Contents(baseUrl & "/usage-queries?contextId=" & _contextId, [ Headers = headers, Content = payload ]) 20 | in 21 | Json.Document(response), 22 | 23 | GetUsageQueryResults = (queryId as text, baseUrl as text) as table => 24 | let 25 | Collate = (offset as number, limit as number, accumulatedTable as table) as table => 26 | let 27 | page = GetUsageQueryResultsPage(queryId, baseUrl, offset, limit), 28 | _table = Table.FromRows(page[rows], page[columns]), 29 | total = page[pagination][totalResults], 30 | newAccumulatedTable = Table.Combine({accumulatedTable, _table}) 31 | in 32 | if offset + limit < total then 33 | @Collate(offset + limit, limit, newAccumulatedTable) 34 | else 35 | newAccumulatedTable 36 | in 37 | Collate(0, 100, #table({}, {})), 38 | 39 | GetUsageQueryResultsPage = (queryId as text, baseUrl as text, optional offset as number, optional limit as number) as record => 40 | let 41 | queryParams = "offset=" & (Number.ToText(offset) ?? "0") & "&" & "limit=" & (Number.ToText(limit) ?? "100"), 42 | url = baseUrl & "/usage-queries/" & queryId & "?" & queryParams, 43 | Poll = (maxAttempts as number, delayInSeconds as number, attempt as number) as any => 44 | let 45 | response = Web.Contents(url), 46 | json = Json.Document(response), 47 | status = Record.FieldOrDefault(json, "status", ""), 48 | result = if status = "COMPLETED" then 49 | json 50 | else if status = "EXPIRED" then 51 | error "Query expired" 52 | else if status = "ERROR" then 53 | error "Query failed" 54 | else if attempt < maxAttempts then // status is either SUBMITTED or RUNNING 55 | Function.InvokeAfter( 56 | () => @Poll(maxAttempts, delayInSeconds, attempt + 1), 57 | #duration(0, 0, 0, delayInSeconds) 58 | ) 59 | else 60 | error "Query timed out" 61 | in 62 | result 63 | in 64 | Poll(10, 5, 0) 65 | in 66 | [ 67 | GetContexts = GetContexts, 68 | RunUsageQuery = RunUsageQuery, 69 | GetUsageQueryResults = GetUsageQueryResults 70 | ] -------------------------------------------------------------------------------- /connectors/PremiumReportingConnector/PremiumReportingConnector.pq: -------------------------------------------------------------------------------- 1 | [Version = "0.0.1"] 2 | section PremiumReportingConnector; 3 | 4 | PremiumReportingConnector = [ 5 | Authentication = [ 6 | OAuth = [ 7 | StartLogin = OAuth[StartLogin], 8 | FinishLogin = OAuth[FinishLogin], 9 | Refresh = OAuth[RefreshToken] 10 | ] 11 | ] 12 | ]; 13 | 14 | PremiumReportingConnector.Publish = [ 15 | Beta = true, 16 | Category = "Other", 17 | ButtonText = {Extension.LoadString("ButtonTitle"), Extension.LoadString("ButtonHelp")}, 18 | LearnMoreUrl = "https://github.com/autodesk-platform-services/aps-powerbi-tools", 19 | SourceImage = PremiumReportingConnector.Icons, 20 | SourceTypeImage = PremiumReportingConnector.Icons 21 | ]; 22 | 23 | PremiumReportingConnector.Icons = [ 24 | Icon16 = { Extension.Contents("PremiumReportingConnector16.png"), Extension.Contents("PremiumReportingConnector20.png"), Extension.Contents("PremiumReportingConnector24.png"), Extension.Contents("PremiumReportingConnector32.png") }, 25 | Icon32 = { Extension.Contents("PremiumReportingConnector32.png"), Extension.Contents("PremiumReportingConnector40.png"), Extension.Contents("PremiumReportingConnector48.png"), Extension.Contents("PremiumReportingConnector64.png") } 26 | ]; 27 | 28 | [DataSource.Kind = "PremiumReportingConnector", Publish = "PremiumReportingConnector.Publish"] 29 | shared PremiumReportingConnector.Contents = Value.ReplaceType(PremiumReportingConnectorImpl, PremiumReportingConnectorType); 30 | 31 | PremiumReportingConnectorImpl = (baseUrl as text, optional options as record) => GetContextsNavigationTable(baseUrl, options); 32 | 33 | PremiumReportingConnectorType = type function ( 34 | baseUrl as (type text meta [ 35 | Documentation.FieldCaption = "Environment", 36 | Documentation.FieldDescription = "Backend environment. Currently only 'V1 (production)' is supported.", 37 | Documentation.AllowedValues = { 38 | "https://developer.api.autodesk.com/insights/v1" meta [ Documentation.Caption = "V1 (production)" ] 39 | } 40 | ]), 41 | optional options as ( 42 | type nullable [ 43 | optional where = (type text meta [ 44 | Documentation.FieldCaption = "Filter", 45 | Documentation.FieldDescription = "Filter to be used in the 'where' clause in usage queries.", 46 | Documentation.SampleValues = { 47 | "usageDate >= '2024-01-01' AND usageDate <= '2024-12-31'" 48 | } 49 | ]) 50 | ] meta [ 51 | Documentation.FieldCaption = "Options" 52 | ] 53 | ) 54 | ) as table meta [ 55 | Documentation.Name = "APS Premium Reporting API Connector", 56 | Documentation.LongDescription = "Connect to Premium Reporting API in Autodesk Platform Services." 57 | ]; 58 | 59 | GetContextsNavigationTable = (baseUrl as text, optional options as record) as table => 60 | let 61 | contexts = PremiumReporting.GetContexts(baseUrl), 62 | treeNodes = List.Transform(contexts, each [ 63 | Name = _[alias], 64 | Key = _[contextId], 65 | Data = GetUsageQueriesNavigationTable(_[contextId], baseUrl, options), 66 | ItemKind = "View", 67 | ItemName = "View", 68 | IsLeaf = false 69 | ]) 70 | in 71 | NavigationTable.FromRecords(treeNodes); 72 | 73 | GetUsageQueriesNavigationTable = (contextId as text, baseUrl as text, optional options as record) as table => 74 | let 75 | GetUsage = (fields as list, metrics as list) as table => 76 | let 77 | query = PremiumReporting.RunUsageQuery(contextId, fields, metrics, baseUrl, options) 78 | in 79 | PremiumReporting.GetUsageQueryResults(query[id], baseUrl), 80 | queries = { 81 | { "Latest usage date per product and customer", "q1", { "productName", "contextId", "fullName" }, { "latestUsageDate" } }, 82 | { "Number of unique users per product and customer", "q2", { "productName", "contextId", "fullName" }, { "uniqueUsers" } }, 83 | { "Number of unique days per product and customer", "q3", { "userName", "productName", "contextId" }, { "totalUniqueDays" } }, 84 | { "Number of unique products used per customer", "q4", { "userName", "contextId" }, { "uniqueProducts" } }, 85 | { "Number of tokens used per flex product and customer", "q5", { "productName", "userName", "contextId" }, { "totalTokens" } } 86 | }, 87 | treeNodes = List.Transform(queries, each [ Name = _{0}, Key = _{1}, Data = GetUsage(_{2}, _{3}), ItemKind = "Function", ItemName = "Function", IsLeaf = true ]) 88 | in 89 | NavigationTable.FromRecords(treeNodes); 90 | 91 | // 92 | // Load common library functions 93 | // 94 | // TEMPORARY WORKAROUND until we're able to reference other M modules 95 | Extension.LoadFunction = (name as text) => 96 | let 97 | binary = Extension.Contents(name), asText = Text.FromBinary(binary) 98 | in 99 | Expression.Evaluate(asText, #shared); 100 | 101 | NavigationTable = Extension.LoadFunction("NavigationTable.pqm"); 102 | NavigationTable.FromTable = NavigationTable[FromTable]; 103 | NavigationTable.FromRecords = NavigationTable[FromRecords]; 104 | OAuth = Extension.LoadFunction("OAuthPKCE.pqm"); 105 | PremiumReporting = Extension.LoadFunction("PremiumReporting.pqm"); 106 | PremiumReporting.GetContexts = PremiumReporting[GetContexts]; 107 | PremiumReporting.RunUsageQuery = PremiumReporting[RunUsageQuery]; 108 | PremiumReporting.GetUsageQueryResults = PremiumReporting[GetUsageQueryResults]; -------------------------------------------------------------------------------- /connectors/PremiumReportingConnector/PremiumReportingConnector.proj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(MSBuildProjectDirectory)\bin\AnyCPU\Debug\ 5 | $(MSBuildProjectDirectory)\obj\ 6 | $(IntermediateOutputPath)MEZ\ 7 | $(OutputPath)$(MsBuildProjectName).mez 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /connectors/PremiumReportingConnector/PremiumReportingConnector.query.pq: -------------------------------------------------------------------------------- 1 | let 2 | result = PremiumReportingConnector.Contents("https://developer.api.autodesk.com/insights/v1") 3 | in 4 | result 5 | -------------------------------------------------------------------------------- /connectors/PremiumReportingConnector/README.md: -------------------------------------------------------------------------------- 1 | # APS Premium Reporting API Connector 2 | 3 | Example [custom Power BI data connector](https://learn.microsoft.com/en-us/power-bi/connect-data/desktop-connector-extensibility) for accessing Autodesk product usage data using [Premium Reporting API](https://aps.autodesk.com/developer/overview/premium-reporting-api). 4 | 5 | ## Usage 6 | 7 | See [Premium Reporting API Connector Usage](https://github.com/autodesk-platform-services/aps-powerbi-tools/wiki/Premium-Reporting-API-Connector-Usage). 8 | 9 | ## Development 10 | 11 | ### Prerequisites 12 | 13 | - [APS application](https://aps.autodesk.com/en/docs/oauth/v2/tutorials/create-app) of type _Desktop, Mobile, Single-Page App_ (this project uses [PKCE authentication](https://aps.autodesk.com/en/docs/oauth/v2/developers_guide/App-types/native/)) 14 | - As per the [documentation](https://aps.autodesk.com/en/docs/insights/v1/tutorials/queries/), you must be a _Premium Team admin_ 15 | - [.NET 8](https://dotnet.microsoft.com/en-us/download/dotnet/8.0) 16 | 17 | ### Building 18 | 19 | - Create a _config.json_ file in the project folder, and populate it with your APS application client ID: 20 | 21 | ```json 22 | { 23 | "APS_CLIENT_ID": "" 24 | } 25 | ``` 26 | 27 | - Register the following Callback URL to your APS application: 28 | 29 | ``` 30 | https://oauth.powerbi.com/views/oauthredirect.html 31 | ``` 32 | 33 | - Build the connector *.mez file (using bash or PowerShell) 34 | 35 | ```bash 36 | dotnet build 37 | ``` 38 | 39 | ### Deploying 40 | 41 | - Copy the generated *.mez file from the _bin/AnyCPU/Debug_ subfolder into Power BI Desktop application as explained [here](https://learn.microsoft.com/en-us/power-bi/connect-data/desktop-connector-extensibility#custom-connectors) 42 | - When selecting data sources in Power BI Desktop, the custom connector will be available under _Other > APS Premium Reporting API Connector (Beta) (Custom)_ 43 | 44 | ### Testing (Visual Studio Code) 45 | 46 | - Make sure you have the [Power Query SDK](https://learn.microsoft.com/en-us/power-query/install-sdk) extension installed 47 | 48 | ![Set credential](./docs/set-credential.png) 49 | 50 | - Create new credentials by clicking the _Set credential_ option (you will be prompted to log in with your Autodesk account) 51 | - Open the [PremiumReportingConnector.query.pq](./PremiumReportingConnector.query.pq) file 52 | - Run the test query by clicking the _Evaluate current file_ option 53 | 54 | ## License 55 | 56 | This sample is licensed under the terms of the [MIT License](http://opensource.org/licenses/MIT). Please see the [LICENSE](LICENSE) file for more details. 57 | -------------------------------------------------------------------------------- /connectors/PremiumReportingConnector/docs/set-credential.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autodesk-platform-services/aps-powerbi-tools/3bc6aeb22e20344ba0c865829bd5af8bd39aba8c/connectors/PremiumReportingConnector/docs/set-credential.png -------------------------------------------------------------------------------- /connectors/PremiumReportingConnector/icons/PremiumReportingConnector16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autodesk-platform-services/aps-powerbi-tools/3bc6aeb22e20344ba0c865829bd5af8bd39aba8c/connectors/PremiumReportingConnector/icons/PremiumReportingConnector16.png -------------------------------------------------------------------------------- /connectors/PremiumReportingConnector/icons/PremiumReportingConnector20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autodesk-platform-services/aps-powerbi-tools/3bc6aeb22e20344ba0c865829bd5af8bd39aba8c/connectors/PremiumReportingConnector/icons/PremiumReportingConnector20.png -------------------------------------------------------------------------------- /connectors/PremiumReportingConnector/icons/PremiumReportingConnector24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autodesk-platform-services/aps-powerbi-tools/3bc6aeb22e20344ba0c865829bd5af8bd39aba8c/connectors/PremiumReportingConnector/icons/PremiumReportingConnector24.png -------------------------------------------------------------------------------- /connectors/PremiumReportingConnector/icons/PremiumReportingConnector32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autodesk-platform-services/aps-powerbi-tools/3bc6aeb22e20344ba0c865829bd5af8bd39aba8c/connectors/PremiumReportingConnector/icons/PremiumReportingConnector32.png -------------------------------------------------------------------------------- /connectors/PremiumReportingConnector/icons/PremiumReportingConnector40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autodesk-platform-services/aps-powerbi-tools/3bc6aeb22e20344ba0c865829bd5af8bd39aba8c/connectors/PremiumReportingConnector/icons/PremiumReportingConnector40.png -------------------------------------------------------------------------------- /connectors/PremiumReportingConnector/icons/PremiumReportingConnector48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autodesk-platform-services/aps-powerbi-tools/3bc6aeb22e20344ba0c865829bd5af8bd39aba8c/connectors/PremiumReportingConnector/icons/PremiumReportingConnector48.png -------------------------------------------------------------------------------- /connectors/PremiumReportingConnector/icons/PremiumReportingConnector64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autodesk-platform-services/aps-powerbi-tools/3bc6aeb22e20344ba0c865829bd5af8bd39aba8c/connectors/PremiumReportingConnector/icons/PremiumReportingConnector64.png -------------------------------------------------------------------------------- /connectors/PremiumReportingConnector/icons/PremiumReportingConnector80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autodesk-platform-services/aps-powerbi-tools/3bc6aeb22e20344ba0c865829bd5af8bd39aba8c/connectors/PremiumReportingConnector/icons/PremiumReportingConnector80.png -------------------------------------------------------------------------------- /connectors/PremiumReportingConnector/resources.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | Connect to Premium Reporting API in Autodesk Platform Services. 122 | 123 | 124 | APS Premium Reporting API Connector 125 | 126 | 127 | -------------------------------------------------------------------------------- /connectors/Shared/Diagnostics.pqm: -------------------------------------------------------------------------------- 1 | // TODO: https://github.com/microsoft/DataConnectors/blob/master/samples/TripPin/8-Diagnostics/Diagnostics.pqm -------------------------------------------------------------------------------- /connectors/Shared/NavigationTable.pqm: -------------------------------------------------------------------------------- 1 | let 2 | FromTable = (_table as table, keyColumns as list, nameColumn as text, dataColumn as text, itemKindColumn as text, itemNameColumn as text, isLeafColumn as text) as table => 3 | let 4 | tableType = Value.Type(_table), 5 | newTableType = Type.AddTableKey(tableType, keyColumns, true) meta 6 | [ 7 | NavigationTable.NameColumn = nameColumn, 8 | NavigationTable.DataColumn = dataColumn, 9 | NavigationTable.ItemKindColumn = itemKindColumn, 10 | Preview.DelayColumn = itemNameColumn, 11 | NavigationTable.IsLeafColumn = isLeafColumn 12 | ], 13 | navigationTable = Value.ReplaceType(_table, newTableType) 14 | in 15 | navigationTable, 16 | 17 | FromRecords = (records as list) as table => FromTable(Table.FromRecords(records), {"Key"}, "Name", "Data", "ItemKind", "ItemName", "IsLeaf"), 18 | RecordType = type [ Name = text, Key = text, Data = any, ItemKind = text, ItemName = text, IsLeaf = logical ], 19 | ListType = type { RecordType }, 20 | FromRecordsType = type function (records as ListType) as any 21 | in 22 | [ 23 | FromTable = FromTable, 24 | FromRecords = Value.ReplaceType(FromRecords, FromRecordsType) 25 | ] -------------------------------------------------------------------------------- /connectors/Shared/OAuth.pqm: -------------------------------------------------------------------------------- 1 | // OAuth requirements: 2 | // 1. Create an APS application 3 | // 2. Register the following Callback URL: "https://oauth.powerbi.com/views/oauthredirect.html" 4 | // 3. Create a _config.json_ with { "APS_CLIENT_ID": "your client id", "APS_CLIENT_SECRET": "your client secret" } 5 | 6 | let 7 | AUTH_BASE_URL = "https://developer.api.autodesk.com/authentication/v2/", 8 | REDIRECT_URI = "https://oauth.powerbi.com/views/oauthredirect.html", 9 | CONFIG = Json.Document(Extension.Contents("config.json")), 10 | APS_CLIENT_ID = if CONFIG[APS_CLIENT_ID]? = null then error "Missing APS_CLIENT_ID" else CONFIG[APS_CLIENT_ID], 11 | APS_CLIENT_SECRET = if CONFIG[APS_CLIENT_SECRET]? = null then error "Missing APS_CLIENT_SECRET" else CONFIG[APS_CLIENT_SECRET], 12 | SCOPES = "data:read", 13 | WINDOW_WIDTH = 600, 14 | WINDOW_HEIGHT = 600, 15 | 16 | StartLogin = (dataSourcePath, state, display) => 17 | let 18 | query = [ 19 | response_type = "code", 20 | client_id = APS_CLIENT_ID, 21 | scope = SCOPES, 22 | redirect_uri = REDIRECT_URI, 23 | state = state 24 | ] 25 | in 26 | [ 27 | LoginUri = Uri.Combine(AUTH_BASE_URL, "authorize") & "?" & Uri.BuildQueryString(query), 28 | CallbackUri = REDIRECT_URI, 29 | Context = null, 30 | WindowWidth = WINDOW_WIDTH, 31 | WindowHeight = WINDOW_HEIGHT 32 | ], 33 | 34 | FinishLogin = (context, callbackUri, state) => 35 | let 36 | parts = Uri.Parts(callbackUri)[Query] 37 | in 38 | TokenMethod(parts[code]), 39 | 40 | TokenMethod = (code) => 41 | let 42 | query = [ 43 | grant_type = "authorization_code", 44 | code = code, 45 | redirect_uri = REDIRECT_URI 46 | ], 47 | response = Web.Contents( 48 | Uri.Combine(AUTH_BASE_URL, "authorize"), 49 | [ 50 | Content = Text.ToBinary(Uri.BuildQueryString(query)), 51 | Headers = [ 52 | #"Content-Type" = "application/x-www-form-urlencoded", 53 | #"Accept" = "application/json", 54 | #"Authorization" = "Basic " & Binary.ToText(Text.ToBinary(APS_CLIENT_ID & ":" & APS_CLIENT_SECRET), BinaryEncoding.Base64) 55 | ] 56 | ] 57 | ) 58 | in 59 | Json.Document(response), 60 | 61 | RefreshToken = (dataSourcePath, refreshToken) => 62 | let 63 | query = [ 64 | grant_type = "refresh_token", 65 | refresh_token = refreshToken 66 | ], 67 | response = Web.Contents( 68 | Uri.Combine(AUTH_BASE_URL, "token"), 69 | [ 70 | Content = Text.ToBinary(Uri.BuildQueryString(query)), 71 | Headers = [ 72 | #"Content-Type" = "application/x-www-form-urlencoded", 73 | #"Accept" = "application/json", 74 | #"Authorization" = "Basic " & Binary.ToText(Text.ToBinary(APS_CLIENT_ID & ":" & APS_CLIENT_SECRET), BinaryEncoding.Base64) 75 | ] 76 | ] 77 | ) 78 | in 79 | Json.Document(response) 80 | in 81 | [ 82 | StartLogin = StartLogin, 83 | FinishLogin = FinishLogin, 84 | RefreshToken = RefreshToken 85 | ] -------------------------------------------------------------------------------- /connectors/Shared/OAuthPKCE.pqm: -------------------------------------------------------------------------------- 1 | // PKCE OAuth requirements: 2 | // 1. Create a "Desktop, Mobile, Single-Page App" APS application 3 | // 2. Register the following Callback URL: "https://oauth.powerbi.com/views/oauthredirect.html" 4 | // 3. Create a _config.json_ with { "APS_CLIENT_ID": "your client id" } 5 | 6 | let 7 | AUTH_BASE_URL = "https://developer.api.autodesk.com/authentication/v2/", 8 | REDIRECT_URI = "https://oauth.powerbi.com/views/oauthredirect.html", 9 | CONFIG = Json.Document(Extension.Contents("config.json")), 10 | APS_CLIENT_ID = if CONFIG[APS_CLIENT_ID]? = null then error "Missing APS_CLIENT_ID" else CONFIG[APS_CLIENT_ID], 11 | SCOPES = "data:read", 12 | WINDOW_WIDTH = 600, 13 | WINDOW_HEIGHT = 600, 14 | 15 | Base64Encode = (s) => Text.Replace(Text.Replace(Text.BeforeDelimiter(Binary.ToText(s, BinaryEncoding.Base64), "="), "+", "-"), "/", "_"), 16 | 17 | StartLogin = (dataSourcePath, state, display) => 18 | let 19 | codeVerifier = Text.NewGuid() & Text.NewGuid(), 20 | codeChallenge = Base64Encode(Crypto.CreateHash(CryptoAlgorithm.SHA256, Text.ToBinary(codeVerifier))), 21 | query = [ 22 | response_type = "code", 23 | client_id = APS_CLIENT_ID, 24 | redirect_uri = REDIRECT_URI, 25 | scope = SCOPES, 26 | code_challenge_method = "S256", 27 | code_challenge = codeChallenge 28 | ] 29 | in 30 | [ 31 | LoginUri = Uri.Combine(AUTH_BASE_URL, "authorize") & "?" & Uri.BuildQueryString(query), 32 | CallbackUri = REDIRECT_URI, 33 | Context = codeVerifier, 34 | WindowWidth = WINDOW_WIDTH, 35 | WindowHeight = WINDOW_HEIGHT 36 | ], 37 | 38 | FinishLogin = (context, callbackUri, state) => 39 | let 40 | parts = Uri.Parts(callbackUri)[Query] 41 | in 42 | TokenMethod(parts[code], "authorization_code", context), 43 | 44 | TokenMethod = (code, grantType, optional context) => 45 | let 46 | codeVerifier = if context <> null then [code_verifier = context] else [], 47 | codeParameter = if grantType = "authorization_code" then [code = code] else [refresh_token = code], 48 | query = [ 49 | client_id = APS_CLIENT_ID, 50 | grant_type = grantType, 51 | redirect_uri = REDIRECT_URI 52 | ], 53 | queryString = Uri.BuildQueryString(codeVerifier & codeParameter & query), 54 | response = Web.Contents( 55 | Uri.Combine(AUTH_BASE_URL, "token"), 56 | [ 57 | Content = Text.ToBinary(queryString), 58 | Headers = [ 59 | #"Content-Type" = "application/x-www-form-urlencoded", 60 | #"Accept" = "application/json" 61 | ] 62 | ] 63 | ), 64 | result = Json.Document(response) 65 | in 66 | if (result[error]? <> null) then 67 | error Error.Record(result[error], result[message]?) 68 | else 69 | result, 70 | 71 | RefreshToken = (dataSourcePath, refreshToken) => TokenMethod(refreshToken, "refresh_token") 72 | in 73 | [ 74 | StartLogin = StartLogin, 75 | FinishLogin = FinishLogin, 76 | RefreshToken = RefreshToken 77 | ] -------------------------------------------------------------------------------- /connectors/TokenFlexConnector/.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | obj 3 | secrets.json 4 | config.json 5 | .DS_Store 6 | Thumbs.db 7 | -------------------------------------------------------------------------------- /connectors/TokenFlexConnector/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "powerquery.sdk.defaultQueryFile": "${workspaceFolder}\\${workspaceFolderBasename}.query.pq", 3 | "powerquery.sdk.defaultExtension": "${workspaceFolder}\\bin\\AnyCPU\\Debug\\${workspaceFolderBasename}.mez" 4 | } 5 | -------------------------------------------------------------------------------- /connectors/TokenFlexConnector/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 Autodesk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /connectors/TokenFlexConnector/README.md: -------------------------------------------------------------------------------- 1 | # APS Token Flex API Connector 2 | 3 | Example [custom Power BI data connector](https://learn.microsoft.com/en-us/power-bi/connect-data/desktop-connector-extensibility) for accessing Autodesk product usage data using [Token Flex API](https://aps.autodesk.com/en/docs/tokenflex/v1/developers_guide/overview/). 4 | 5 | ## Usage 6 | 7 | See [Token Flex API Connector Usage](https://github.com/autodesk-platform-services/aps-powerbi-tools/wiki/Token-Flex-API-Connector-Usage). 8 | 9 | ## Development 10 | 11 | ### Prerequisites 12 | 13 | - [APS application](https://aps.autodesk.com/en/docs/oauth/v2/tutorials/create-app) of type _Desktop, Mobile, Single-Page App_ (this project uses [PKCE authentication](https://aps.autodesk.com/en/docs/oauth/v2/developers_guide/App-types/native/)) 14 | - As per the [documentation](https://aps.autodesk.com/en/docs/tokenflex/v1/developers_guide/authentication/), you must be an _Enterprise Team admin_ 15 | - [.NET 8](https://dotnet.microsoft.com/en-us/download/dotnet/8.0) 16 | 17 | ### Building 18 | 19 | - Create a _config.json_ file in the project folder, and populate it with your APS application client ID: 20 | 21 | ```json 22 | { 23 | "APS_CLIENT_ID": "" 24 | } 25 | ``` 26 | 27 | - Register the following Callback URL to your APS application: 28 | 29 | ``` 30 | https://oauth.powerbi.com/views/oauthredirect.html 31 | ``` 32 | 33 | - Build the connector *.mez file (using bash or PowerShell) 34 | 35 | ```bash 36 | dotnet build 37 | ``` 38 | 39 | ### Deploying 40 | 41 | - Copy the generated *.mez file from the _bin/AnyCPU/Debug_ subfolder into Power BI Desktop application as explained [here](https://learn.microsoft.com/en-us/power-bi/connect-data/desktop-connector-extensibility#custom-connectors) 42 | - When selecting data sources in Power BI Desktop, the custom connector will be available under _Other > APS Token Flex API Connector (Beta) (Custom)_ 43 | 44 | ### Testing (Visual Studio Code) 45 | 46 | - Make sure you have the [Power Query SDK](https://learn.microsoft.com/en-us/power-query/install-sdk) extension installed 47 | 48 | ![Set credential](./docs/set-credential.png) 49 | 50 | - Create new credentials by clicking the _Set credential_ option (you will be prompted to log in with your Autodesk account) 51 | - Open the [TokenFlexConnector.query.pq](./TokenFlexConnector.query.pq) file 52 | - Run the test query by clicking the _Evaluate current file_ option 53 | 54 | ## License 55 | 56 | This sample is licensed under the terms of the [MIT License](http://opensource.org/licenses/MIT). Please see the [LICENSE](LICENSE) file for more details. 57 | -------------------------------------------------------------------------------- /connectors/TokenFlexConnector/TokenFlex.pqm: -------------------------------------------------------------------------------- 1 | let 2 | GetContracts = (api as text) => 3 | let 4 | response = Web.Contents(api & "/contract") 5 | in 6 | Json.Document(response), 7 | 8 | RunUsageQuery = (contractNumber as text, _fields as list, _metrics as list, _usageCategory as list, baseUrl as text, optional options as record) => 9 | let 10 | _options = options ?? [], 11 | headers = [#"Content-Type" = "application/json"], 12 | payload = Json.FromValue([ 13 | fields = _fields, 14 | metrics = _metrics, 15 | usageCategory = _usageCategory, 16 | where = _options[where] ?? "" 17 | ]), 18 | response = Web.Contents(baseUrl & "/usage/" & contractNumber & "/query", [ Headers = headers, Content = payload ]) 19 | in 20 | Json.Document(response), 21 | 22 | GetUsageQueryResults = (contractNumber as text, queryId as text, baseUrl as text) as table => 23 | let 24 | Collate = (offset as number, limit as number, accumulatedTable as table) as table => 25 | let 26 | page = GetUsageQueryResultsPage(contractNumber, queryId, baseUrl, offset, limit), 27 | _table = Table.FromRows(page[result][rows], page[result][columns]), 28 | total = page[result][total], 29 | newAccumulatedTable = Table.Combine({accumulatedTable, _table}) 30 | in 31 | if offset + limit < total then 32 | @Collate(offset + limit, limit, newAccumulatedTable) 33 | else 34 | newAccumulatedTable 35 | in 36 | Collate(0, 100, #table({}, {})), 37 | 38 | GetUsageQueryResultsPage = (contractNumber as text, queryId as text, baseUrl as text, optional offset as number, optional limit as number) as record => 39 | let 40 | queryParams = "offset=" & (Number.ToText(offset) ?? "0") & "&" & "limit=" & (Number.ToText(limit) ?? "100"), 41 | url = baseUrl & "/usage/" & contractNumber & "/query/" & queryId & "?" & queryParams, 42 | Poll = (maxAttempts as number, delayInSeconds as number, attempt as number) as any => 43 | let 44 | response = Web.Contents(url), 45 | json = Json.Document(response), 46 | status = Record.FieldOrDefault(json, "status", ""), 47 | result = if status = "DONE" then 48 | json 49 | else if status = "FAILED" then 50 | error "Query failed" 51 | else if status = "TIMEOUT" then 52 | error "Query timed out" 53 | else if status = "EXPIRED" then 54 | error "Query expired" 55 | else if attempt < maxAttempts then // status is either QUEUED or PROCESSING 56 | Function.InvokeAfter( 57 | () => @Poll(maxAttempts, delayInSeconds, attempt + 1), 58 | #duration(0, 0, 0, delayInSeconds) 59 | ) 60 | else 61 | error "Query timed out" 62 | in 63 | result 64 | in 65 | Poll(10, 5, 0) 66 | in 67 | [ 68 | GetContracts = GetContracts, 69 | RunUsageQuery = RunUsageQuery, 70 | GetUsageQueryResults = GetUsageQueryResults 71 | ] -------------------------------------------------------------------------------- /connectors/TokenFlexConnector/TokenFlexConnector.pq: -------------------------------------------------------------------------------- 1 | [Version = "0.0.1"] 2 | section TokenFlexConnector; 3 | 4 | TokenFlexConnector = [ 5 | Authentication = [ 6 | OAuth = [ 7 | StartLogin = OAuth[StartLogin], 8 | FinishLogin = OAuth[FinishLogin], 9 | Refresh = OAuth[RefreshToken] 10 | ] 11 | ] 12 | ]; 13 | 14 | TokenFlexConnector.Publish = [ 15 | Beta = true, 16 | Category = "Other", 17 | ButtonText = { Extension.LoadString("ButtonTitle"), Extension.LoadString("ButtonHelp") }, 18 | LearnMoreUrl = "https://github.com/autodesk-platform-services/aps-powerbi-tools", 19 | SourceImage = TokenFlexConnector.Icons, 20 | SourceTypeImage = TokenFlexConnector.Icons 21 | ]; 22 | 23 | TokenFlexConnector.Icons = [ 24 | Icon16 = { Extension.Contents("TokenFlexConnector16.png"), Extension.Contents("TokenFlexConnector20.png"), Extension.Contents("TokenFlexConnector24.png"), Extension.Contents("TokenFlexConnector32.png") }, 25 | Icon32 = { Extension.Contents("TokenFlexConnector32.png"), Extension.Contents("TokenFlexConnector40.png"), Extension.Contents("TokenFlexConnector48.png"), Extension.Contents("TokenFlexConnector64.png") } 26 | ]; 27 | 28 | [DataSource.Kind="TokenFlexConnector", Publish="TokenFlexConnector.Publish"] 29 | shared TokenFlexConnector.Contents = Value.ReplaceType(TokenFlexConnectorImpl, TokenFlexConnectorType); 30 | 31 | TokenFlexConnectorImpl = (baseUrl as text, optional options as record) => GetContractsNavigationTable(baseUrl, options); 32 | 33 | TokenFlexConnectorType = type function ( 34 | baseUrl as (type text meta [ 35 | Documentation.FieldCaption = "Environment", 36 | Documentation.FieldDescription = "Backend environment. Currently only 'V1 (production)' is supported.", 37 | Documentation.AllowedValues = { 38 | "https://developer.api.autodesk.com/tokenflex/v1" meta [ Documentation.Caption = "V1 (production)" ] 39 | } 40 | ]), 41 | optional options as ( 42 | type nullable [ 43 | optional where = (type text meta [ 44 | Documentation.FieldCaption = "Filter", 45 | Documentation.FieldDescription = "Filter to be used in the 'where' clause in usage queries.", 46 | Documentation.SampleValues = { 47 | "usageDate >= '2024-01-01' AND usageDate <= '2024-12-31'" 48 | } 49 | ]) 50 | ] meta [ 51 | Documentation.FieldCaption = "Options" 52 | ] 53 | ) 54 | ) as table meta [ 55 | Documentation.Name = "APS Token Flex API Connector", 56 | Documentation.LongDescription = "Connect to Token Flex API in Autodesk Platform Services." 57 | ]; 58 | 59 | GetContractsNavigationTable = (baseUrl as text, optional options as record) => 60 | let 61 | contracts = TokenFlex.GetContracts(baseUrl), 62 | treeNodes = List.Transform(contracts, each [ 63 | Name = _[contractName], 64 | Key = _[contractNumber], 65 | Data = GetUsageQueriesNavigationTable(_[contractNumber], baseUrl, options), 66 | ItemKind = "View", 67 | ItemName = "View", 68 | IsLeaf = false 69 | ]) 70 | in 71 | NavigationTable.FromRecords(treeNodes); 72 | 73 | GetUsageQueriesNavigationTable = (contractNumber as text, baseUrl as text, optional options as record) as table => 74 | let 75 | GetUsage = (fields as list, metrics as list, usageCategory as list) as table => 76 | let 77 | query = TokenFlex.RunUsageQuery(contractNumber, fields, metrics, usageCategory, baseUrl, options) 78 | in 79 | TokenFlex.GetUsageQueryResults(contractNumber, query[id], baseUrl), 80 | queries = { 81 | { "Unique users and tokens consumed per product and customer", "q1", { "userName", "productName" }, { "uniqueUsers", "tokensConsumed" }, { "MANUAL_ADJUSTMENT", "MANUAL_CONSUMPTION" } } 82 | }, 83 | treeNodes = List.Transform(queries, each [ Name = _{0}, Key = _{1}, Data = GetUsage(_{2}, _{3}, _{4}), ItemKind = "Function", ItemName = "Function", IsLeaf = true ]) 84 | in 85 | NavigationTable.FromRecords(treeNodes); 86 | 87 | // 88 | // Load common library functions 89 | // 90 | // TEMPORARY WORKAROUND until we're able to reference other M modules 91 | Extension.LoadFunction = (name as text) => 92 | let 93 | binary = Extension.Contents(name), asText = Text.FromBinary(binary) 94 | in 95 | Expression.Evaluate(asText, #shared); 96 | 97 | NavigationTable = Extension.LoadFunction("NavigationTable.pqm"); 98 | NavigationTable.FromRecords = NavigationTable[FromRecords]; 99 | OAuth = Extension.LoadFunction("OAuthPKCE.pqm"); 100 | TokenFlex = Extension.LoadFunction("TokenFlex.pqm"); 101 | TokenFlex.GetContracts = TokenFlex[GetContracts]; 102 | TokenFlex.RunUsageQuery = TokenFlex[RunUsageQuery]; 103 | TokenFlex.GetUsageQueryResults = TokenFlex[GetUsageQueryResults]; -------------------------------------------------------------------------------- /connectors/TokenFlexConnector/TokenFlexConnector.proj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(MSBuildProjectDirectory)\bin\AnyCPU\Debug\ 5 | $(MSBuildProjectDirectory)\obj\ 6 | $(IntermediateOutputPath)MEZ\ 7 | $(OutputPath)$(MsBuildProjectName).mez 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /connectors/TokenFlexConnector/TokenFlexConnector.query.pq: -------------------------------------------------------------------------------- 1 | // Use this file to write queries to test your data connector 2 | let 3 | result = TokenFlexConnector.Contents("https://developer.api.autodesk.com/tokenflex/v1") 4 | in 5 | result 6 | -------------------------------------------------------------------------------- /connectors/TokenFlexConnector/docs/set-credential.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autodesk-platform-services/aps-powerbi-tools/3bc6aeb22e20344ba0c865829bd5af8bd39aba8c/connectors/TokenFlexConnector/docs/set-credential.png -------------------------------------------------------------------------------- /connectors/TokenFlexConnector/icons/TokenFlexConnector16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autodesk-platform-services/aps-powerbi-tools/3bc6aeb22e20344ba0c865829bd5af8bd39aba8c/connectors/TokenFlexConnector/icons/TokenFlexConnector16.png -------------------------------------------------------------------------------- /connectors/TokenFlexConnector/icons/TokenFlexConnector20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autodesk-platform-services/aps-powerbi-tools/3bc6aeb22e20344ba0c865829bd5af8bd39aba8c/connectors/TokenFlexConnector/icons/TokenFlexConnector20.png -------------------------------------------------------------------------------- /connectors/TokenFlexConnector/icons/TokenFlexConnector24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autodesk-platform-services/aps-powerbi-tools/3bc6aeb22e20344ba0c865829bd5af8bd39aba8c/connectors/TokenFlexConnector/icons/TokenFlexConnector24.png -------------------------------------------------------------------------------- /connectors/TokenFlexConnector/icons/TokenFlexConnector32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autodesk-platform-services/aps-powerbi-tools/3bc6aeb22e20344ba0c865829bd5af8bd39aba8c/connectors/TokenFlexConnector/icons/TokenFlexConnector32.png -------------------------------------------------------------------------------- /connectors/TokenFlexConnector/icons/TokenFlexConnector40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autodesk-platform-services/aps-powerbi-tools/3bc6aeb22e20344ba0c865829bd5af8bd39aba8c/connectors/TokenFlexConnector/icons/TokenFlexConnector40.png -------------------------------------------------------------------------------- /connectors/TokenFlexConnector/icons/TokenFlexConnector48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autodesk-platform-services/aps-powerbi-tools/3bc6aeb22e20344ba0c865829bd5af8bd39aba8c/connectors/TokenFlexConnector/icons/TokenFlexConnector48.png -------------------------------------------------------------------------------- /connectors/TokenFlexConnector/icons/TokenFlexConnector64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autodesk-platform-services/aps-powerbi-tools/3bc6aeb22e20344ba0c865829bd5af8bd39aba8c/connectors/TokenFlexConnector/icons/TokenFlexConnector64.png -------------------------------------------------------------------------------- /connectors/TokenFlexConnector/icons/TokenFlexConnector80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autodesk-platform-services/aps-powerbi-tools/3bc6aeb22e20344ba0c865829bd5af8bd39aba8c/connectors/TokenFlexConnector/icons/TokenFlexConnector80.png -------------------------------------------------------------------------------- /connectors/TokenFlexConnector/resources.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | Connect to Token Flex API in Autodesk Platform Services. 122 | 123 | 124 | APS Token Flex API Connector 125 | 126 | 127 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autodesk-platform-services/aps-powerbi-tools/3bc6aeb22e20344ba0c865829bd5af8bd39aba8c/screenshot.png -------------------------------------------------------------------------------- /services/aps-shares-app/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | *.log 4 | .DS_Store 5 | Thumbs.db 6 | -------------------------------------------------------------------------------- /services/aps-shares-app/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Server", 11 | "skipFiles": [ 12 | "/**" 13 | ], 14 | "program": "${workspaceFolder}/index.js", 15 | "envFile": "${workspaceFolder}/.env" 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /services/aps-shares-app/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 Autodesk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /services/aps-shares-app/README.md: -------------------------------------------------------------------------------- 1 | # APS Shares App 2 | 3 | Simple web application providing public read-only access to selected designs in [APS](https://aps.autodesk.com)-based applications such as [Autodesk Construction Cloud](https://construction.autodesk.com). 4 | 5 | ![Screenshot](./screenshot.png) 6 | 7 | ### How does it work? 8 | 9 | After logging in with Autodesk credentials, users can create _shares_ for specific designs that they have access to. Each _share_ object provides a publicly accessible link that will always generate a fresh 2-legged token with read-only access to just the URN of the corresponding design. Later, individual _shares_ can be deleted, invalidating their corresponding public links as well. 10 | 11 | > The share objects are currently persisted in an [automatically generated OSS bucket](./config.js#L7). The logic for saving and loading shares is implemented in the [./shares.js](./shares.js) file. You can modify this logic to store the shares elsewhere, for example, in a MongoDB database. 12 | 13 | ## Try it out 14 | 15 | You can try a live demo running on this page: https://aps-shares-app.autodesk.io. Log in with your Autodesk credentials, and hit the _New Share_ button. Note that in order for the application to have access to the designs you want to share, you will need to [provision access to your ACC or BIM360 project](https://tutorials.autodesk.io/#provision-access-in-other-products) for it. You'll find the application client ID in the _New Share_ dialog. 16 | 17 | Alternatively, you can fork this application on the [replit.com](https://replit.com) website: https://replit.com/@petrbroz1/APS-Shares-App, and provide your own APS application credentials as environment variables. All the required environment variables are listed in the [Running locally](#running-locally) section below. 18 | 19 | ## Development 20 | 21 | ### Prerequisites 22 | 23 | - [APS app credentials](https://forge.autodesk.com/en/docs/oauth/v2/tutorials/create-app) 24 | - [Provision access to ACC or BIM360](https://tutorials.autodesk.io/#provision-access-in-other-products) 25 | - [Node.js](https://nodejs.org) (ideally the _Long Term Support_ version) 26 | - Terminal (for example, [Windows Command Prompt](https://en.wikipedia.org/wiki/Cmd.exe) or [macOS Terminal](https://support.apple.com/guide/terminal/welcome/mac)) 27 | 28 | ### Running locally 29 | 30 | - Clone this repository 31 | - Install dependencies: `npm install` 32 | - Setup environment variables: 33 | - `APS_CLIENT_ID` - your APS application client ID 34 | - `APS_CLIENT_SECRET` - your APS application client secret 35 | - `APS_CALLBACK_URL` - URL for users to be redirected to after they log in; it should be the origin of your application followed by `/auth/callback`, for example, `http://localhost:8080/auth/callback` 36 | - `APS_APP_NAME` - your APS application name; it will be displayed in provisioning instructions in the UI 37 | - `SERVER_SESSION_SECRET` - a random string used to encipher/decipher sensitive data 38 | - Run the server: `npm start` 39 | - Open the browser and navigate to http://localhost:8080 40 | 41 | > Tip: when using [Visual Studio Code](https://code.visualstudio.com), you can specify the environment variables listed above in a _.env_ file in the project folder, and run & debug the application directly from the editor. 42 | 43 | ## FAQ 44 | 45 | ### How do I specify the design to share? 46 | 47 | To keep the application simple and easy to understand (and customize), there is no UI for project browsing and design selection. Instead, users will need to specify the base64-encoded URN of the design to share directly. You can easily retrieve the URN after loading the design into any APS-based application. For example, after opening your design in [Autodesk Construction Cloud](https://construction.autodesk.com), open the browser console and type `NOP_VIEWER.model.getData().urn` to retrieve the URN. 48 | 49 | ## Troubleshooting 50 | 51 | Please contact us via https://aps.autodesk.com/get-help. 52 | 53 | ## License 54 | 55 | This sample is licensed under the terms of the [MIT License](http://opensource.org/licenses/MIT). Please see the [LICENSE](LICENSE) file for more details. 56 | -------------------------------------------------------------------------------- /services/aps-shares-app/config.js: -------------------------------------------------------------------------------- 1 | const { APS_CLIENT_ID, APS_CLIENT_SECRET, APS_CALLBACK_URL, APS_APP_NAME, SERVER_SESSION_SECRET } = process.env; 2 | if (!APS_CLIENT_ID || !APS_CLIENT_SECRET || !APS_CALLBACK_URL || !APS_APP_NAME || !SERVER_SESSION_SECRET) { 3 | console.error('Missing some of the environment variables.'); 4 | process.exit(1); 5 | } 6 | const PORT = process.env.PORT || 8080; 7 | const APS_BUCKET_KEY = process.env.APS_BUCKET_KEY || APS_CLIENT_ID.toLowerCase() + '-shares'; 8 | 9 | module.exports = { 10 | APS_CLIENT_ID, 11 | APS_CLIENT_SECRET, 12 | APS_CALLBACK_URL, 13 | APS_APP_NAME, 14 | APS_BUCKET_KEY, 15 | SERVER_SESSION_SECRET, 16 | PORT 17 | }; 18 | -------------------------------------------------------------------------------- /services/aps-shares-app/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const morgan = require('morgan'); 3 | const session = require('cookie-session'); 4 | const { SERVER_SESSION_SECRET, PORT } = require('./config.js'); 5 | 6 | let app = express(); 7 | app.set('view engine', 'ejs'); 8 | app.use(morgan('tiny')); 9 | app.use(session({ secret: SERVER_SESSION_SECRET, maxAge: 24 * 60 * 60 * 1000 })); 10 | app.use(require('./routes/auth.js')); 11 | app.use(require('./routes/shares.js')); 12 | app.use(require('./routes/token.js')); 13 | app.use((err, req, res, next) => { 14 | console.error(err.stack); 15 | res.status(500).render('error', { user: req.session.user, error: err }); 16 | }) 17 | app.listen(PORT, () => console.log(`Server listening on port ${PORT}...`)); 18 | -------------------------------------------------------------------------------- /services/aps-shares-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aps-shares-app", 3 | "description": "Simple web application providing public access to selected designs in Autodesk Platform Services.", 4 | "version": "0.0.8", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js" 8 | }, 9 | "keywords": [ 10 | "autodesk-platform-services" 11 | ], 12 | "license": "MIT", 13 | "dependencies": { 14 | "@aps_sdk/authentication": "^0.1.0-beta.1", 15 | "@aps_sdk/autodesk-sdkmanager": "^0.0.7-beta.1", 16 | "@aps_sdk/model-derivative": "^0.1.0-beta.1", 17 | "@aps_sdk/oss": "^0.1.0-beta.1", 18 | "axios": "^1.4.0", 19 | "cookie-session": "^2.0.0", 20 | "cors": "^2.8.5", 21 | "ejs": "^3.1.9", 22 | "express": "^4.18.2", 23 | "morgan": "^1.10.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /services/aps-shares-app/routes/auth.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const { SdkManagerBuilder } = require('@aps_sdk/autodesk-sdkmanager'); 3 | const { AuthenticationClient, ResponseType, Scopes } = require('@aps_sdk/authentication'); 4 | const { APS_CLIENT_ID, APS_CLIENT_SECRET, APS_CALLBACK_URL } = require('../config.js'); 5 | 6 | const sdkManager = SdkManagerBuilder.create().build(); 7 | const authenticationClient = new AuthenticationClient(sdkManager); 8 | const router = express.Router(); 9 | 10 | router.get('/auth/login', (req, res) => { 11 | const url = authenticationClient.authorize(APS_CLIENT_ID, ResponseType.Code, APS_CALLBACK_URL, [Scopes.DataRead]); 12 | res.redirect(url); 13 | }); 14 | 15 | router.get('/auth/callback', async (req, res, next) => { 16 | try { 17 | const credentials = await authenticationClient.getThreeLeggedToken(APS_CLIENT_ID, req.query.code, APS_CALLBACK_URL, { 18 | clientSecret: APS_CLIENT_SECRET 19 | }); 20 | const profile = await authenticationClient.getUserInfo(credentials.access_token); 21 | credentials.expires_at = Date.now() + credentials.expires_in * 1000;; 22 | req.session.credentials = credentials; 23 | req.session.user = { id: profile.sub, name: profile.name }; 24 | res.redirect('/'); 25 | } catch (err) { 26 | next(err); 27 | } 28 | }); 29 | 30 | router.get('/auth/logout', (req, res) => { 31 | req.session = null; 32 | res.redirect('/'); 33 | }); 34 | 35 | module.exports = router; 36 | -------------------------------------------------------------------------------- /services/aps-shares-app/routes/shares.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const { SdkManagerBuilder } = require('@aps_sdk/autodesk-sdkmanager'); 3 | const { AuthenticationClient } = require('@aps_sdk/authentication'); 4 | const { ModelDerivativeClient, Region } = require('@aps_sdk/model-derivative'); 5 | const { listShares, createShare, deleteShare } = require('../shares.js'); 6 | const { APS_CLIENT_ID, APS_CLIENT_SECRET, APS_APP_NAME } = require('../config.js'); 7 | 8 | const sdkManager = SdkManagerBuilder.create().build(); 9 | const authenticationClient = new AuthenticationClient(sdkManager); 10 | const modelDerivativeClient = new ModelDerivativeClient(sdkManager); 11 | const router = express.Router(); 12 | 13 | // Checks whether a 3-legged token representing a specific user has access to given URN. 14 | async function canAccessUrn(urn, credentials) { 15 | try { 16 | let region = Region.Us; 17 | const urnBuffer = Buffer.from(urn, 'base64'); 18 | const objectId = urnBuffer.toString('utf-8'); 19 | 20 | if (objectId?.toLowerCase().includes('emea')) { 21 | region = Region.Emea; 22 | } 23 | 24 | await modelDerivativeClient.getManifest(credentials.access_token, urn, { region }); 25 | } catch (err) { 26 | return false; 27 | } 28 | return true; 29 | } 30 | 31 | router.get('/', async (req, res, next) => { 32 | const { credentials, user } = req.session; 33 | const app = { id: APS_CLIENT_ID, name: APS_APP_NAME }; 34 | if (credentials && user) { 35 | try { 36 | const shares = await listShares(user.id); 37 | res.render('index', { user, shares, app }); 38 | } catch (err) { 39 | next(err); 40 | } 41 | } else { 42 | res.render('index', { user: null, shares: null, app }); 43 | } 44 | }); 45 | 46 | router.use('/shares', async (req, res, next) => { 47 | const { credentials, user } = req.session; 48 | try { 49 | if (credentials && user) { 50 | if (credentials.expires_at < Date.now()) { 51 | const refreshToken = credentials.refresh_token; 52 | const credentials = await authenticationClient.getRefreshToken(APS_CLIENT_ID, refreshToken, { 53 | clientSecret: APS_CLIENT_SECRET 54 | }); 55 | credentials.expires_at = Date.now() + credentials.expires_in * 1000; 56 | req.session.credentials = credentials; 57 | } 58 | req.user_id = user.id; 59 | next(); 60 | } else { 61 | throw new Error('Unauthorized access.'); 62 | } 63 | } catch (err) { 64 | next(err); 65 | } 66 | }); 67 | 68 | router.post('/shares', express.urlencoded({ extended: true }), async (req, res, next) => { 69 | try { 70 | const { urn, description } = req.body; 71 | if (!urn.match(/^[a-zA-Z0-9_]+$/)) { 72 | throw new Error('Incorrect URN.'); 73 | } 74 | if (description && description.length > 512) { 75 | throw new Error('Description is limited to 512 characters.'); 76 | } 77 | const hasAccess = await canAccessUrn(urn, req.session.credentials); 78 | if (!hasAccess) { 79 | throw new Error('URN is incorrect or not accessible.'); 80 | } 81 | await createShare(req.user_id, urn, description); 82 | res.redirect('/'); 83 | } catch (err) { 84 | next(err); 85 | } 86 | }); 87 | 88 | router.delete('/shares/:id', async (req, res, next) => { 89 | try { 90 | const { id } = req.params; 91 | if (!id.match(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/)) { 92 | throw new Error('Incorrect share ID.'); 93 | } 94 | await deleteShare(req.user_id, id); 95 | res.status(200).end(); 96 | } catch (err) { 97 | next(err); 98 | } 99 | }); 100 | 101 | module.exports = router; 102 | -------------------------------------------------------------------------------- /services/aps-shares-app/routes/token.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const axios = require('axios').default; 3 | const cors = require('cors'); 4 | const { listShares, decryptShareCode } = require('../shares.js'); 5 | const { APS_CLIENT_ID, APS_CLIENT_SECRET } = require('../config.js'); 6 | 7 | let router = express.Router(); 8 | 9 | // GET /token?share= 10 | router.get('/token', cors(), async (req, res, next) => { 11 | try { 12 | if (!req.query.share) { 13 | throw new Error(`Missing 'share' query parameter.`); 14 | } 15 | const [ownerId, shareId] = decryptShareCode(req.query.share); 16 | const shares = await listShares(ownerId); 17 | const share = shares.find(s => s.id === shareId); 18 | if (!share) { 19 | res.status(403).end(); 20 | } else { 21 | const payload = { 22 | grant_type: 'client_credentials', 23 | scope: 'data:read:' + Buffer.from(share.urn, 'base64').toString() 24 | }; 25 | const headers = { 26 | 'Authorization': 'Basic ' + Buffer.from(APS_CLIENT_ID + ':' + APS_CLIENT_SECRET).toString('base64'), 27 | 'Content-Type': 'application/x-www-form-urlencoded' 28 | }; 29 | const { data } = await axios.post('https://developer.api.autodesk.com/authentication/v2/token', payload, { headers }); 30 | data.urn = share.urn; 31 | res.json(data); 32 | } 33 | } catch (err) { 34 | next(err); 35 | } 36 | }); 37 | 38 | module.exports = router; 39 | -------------------------------------------------------------------------------- /services/aps-shares-app/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autodesk-platform-services/aps-powerbi-tools/3bc6aeb22e20344ba0c865829bd5af8bd39aba8c/services/aps-shares-app/screenshot.png -------------------------------------------------------------------------------- /services/aps-shares-app/shares.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto'); 2 | const axios = require('axios').default; 3 | const { SdkManagerBuilder } = require('@aps_sdk/autodesk-sdkmanager'); 4 | const { AuthenticationClient, Scopes } = require('@aps_sdk/authentication'); 5 | const { OssClient, CreateBucketXAdsRegionEnum, CreateBucketsPayloadPolicyKeyEnum, CreateSignedResourceAccessEnum } = require('@aps_sdk/oss'); 6 | const { APS_CLIENT_ID, APS_CLIENT_SECRET , APS_BUCKET_KEY, SERVER_SESSION_SECRET } = require('./config.js'); 7 | 8 | const sdkManager = SdkManagerBuilder.create().build(); 9 | const authenticationClient = new AuthenticationClient(sdkManager); 10 | const ossClient = new OssClient(sdkManager); 11 | 12 | let _credentials = null; 13 | async function getAccessToken() { 14 | if (!_credentials || _credentials.expires_at < Date.now()) { 15 | _credentials = await authenticationClient.getTwoLeggedToken(APS_CLIENT_ID, APS_CLIENT_SECRET, [Scopes.BucketCreate, Scopes.BucketRead, Scopes.DataCreate, Scopes.DataWrite, Scopes.DataRead]); 16 | _credentials.expires_at = Date.now() + _credentials.expires_in * 1000; 17 | } 18 | return _credentials.access_token; 19 | } 20 | 21 | async function ensureBucketExists(bucketKey) { 22 | const token = await getAccessToken(); 23 | try { 24 | await ossClient.getBucketDetails(token, bucketKey); 25 | } catch (err) { 26 | if (err.axiosError.response.status === 404) { 27 | await ossClient.createBucket(token, CreateBucketXAdsRegionEnum.Us, { 28 | bucketKey, 29 | policyKey: CreateBucketsPayloadPolicyKeyEnum.Persistent 30 | }); 31 | } else { 32 | throw err; 33 | } 34 | } 35 | } 36 | 37 | async function listShares(ownerId) { 38 | await ensureBucketExists(APS_BUCKET_KEY); 39 | const token = await getAccessToken(); 40 | try { 41 | const { signedUrl } = await ossClient.createSignedResource(token, APS_BUCKET_KEY, ownerId, { access: CreateSignedResourceAccessEnum.Read }); 42 | const { data: shares } = await axios.get(signedUrl); 43 | return shares; 44 | } catch (err) { 45 | if (err.axiosError.response.status === 404) { 46 | return []; 47 | } else { 48 | throw err; 49 | } 50 | } 51 | } 52 | 53 | async function updateShares(ownerId, func) { 54 | let shares = await listShares(ownerId); 55 | shares = func(shares); 56 | const token = await getAccessToken(); 57 | const { signedUrl } = await ossClient.createSignedResource(token, APS_BUCKET_KEY, ownerId, { access: CreateSignedResourceAccessEnum.Write }); 58 | const { data } = await axios.put(signedUrl, JSON.stringify(shares)); 59 | return data; 60 | } 61 | 62 | async function createShare(ownerId, urn, description) { 63 | const id = crypto.randomUUID(); 64 | const code = encryptShareCode(ownerId, id); 65 | const share = { id, ownerId, code, created: new Date(), urn, description }; 66 | await updateShares(ownerId, shares => [...shares, share]); 67 | return share; 68 | } 69 | 70 | async function deleteShare(ownerId, shareId) { 71 | await updateShares(ownerId, shares => shares.filter(share => share.id !== shareId)); 72 | } 73 | 74 | function encryptShareCode(ownerId, shareId) { 75 | const cipher = crypto.createCipher('aes-128-ecb', SERVER_SESSION_SECRET, {}); 76 | return cipher.update(`${ownerId}/${shareId}`, 'utf8', 'hex') + cipher.final('hex'); 77 | } 78 | 79 | function decryptShareCode(code) { 80 | const decipher = crypto.createDecipher('aes-128-ecb', SERVER_SESSION_SECRET); 81 | const decrypted = decipher.update(code, 'hex', 'utf8') + decipher.final('utf8'); 82 | if (!decrypted.match(/^[a-zA-Z0-9]+\/[0-9a-fA-F\-]+$/)) { 83 | throw new Error('Invalid share code.'); 84 | } 85 | return decrypted.split('/'); 86 | } 87 | 88 | module.exports = { 89 | listShares, 90 | createShare, 91 | deleteShare, 92 | encryptShareCode, 93 | decryptShareCode 94 | }; 95 | -------------------------------------------------------------------------------- /services/aps-shares-app/views/error.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Autodesk Platform Services: Shares App 8 | 10 | 11 | 12 | 13 | 28 | 29 |
30 |

Error

31 |

32 | <%= error.message %> 33 |

34 |
35 | 36 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /services/aps-shares-app/views/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Autodesk Platform Services: Shares App 8 | 10 | 11 | 12 | 13 | 14 | 15 | 30 | 31 | 32 |
33 | <% if (user) { %> 34 |
35 | 38 |
39 | <% } %> 40 | 41 | <% if (shares) { %> 42 |

Shares

43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | <% for (const share of shares) { %> 54 | 55 | 56 | 57 | 58 | 69 | 70 | <% } %> 71 | 72 |
IDCreatedDescriptionActions
<%= share.id %><%= share.created %><%= share.description %> 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 |
73 | <% } %> 74 |
75 | 76 | 77 | 118 | 119 | 120 |
121 | 128 |
129 | 130 | 163 | 166 | 167 | 168 | -------------------------------------------------------------------------------- /services/ssa-auth-app/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env* 3 | *.log 4 | .DS_Store 5 | Thumbs.db 6 | -------------------------------------------------------------------------------- /services/ssa-auth-app/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 Autodesk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /services/ssa-auth-app/README.md: -------------------------------------------------------------------------------- 1 | # SSA Auth App 2 | 3 | Simple web application providing authentication for accessing project and design data in [Autodesk Construction Cloud](https://construction.autodesk.com) using _Secure Service Accounts_. One of the use cases of this application is the generation of access tokens for [APS Viewer](https://aps.autodesk.com/en/docs/viewer/v7/developers_guide/overview/) hosted within Power BI reports. 4 | 5 | ## Development 6 | 7 | ### Prerequisites 8 | 9 | - [APS app credentials](https://forge.autodesk.com/en/docs/oauth/v2/tutorials/create-app) 10 | - [Provision access to ACC or BIM360](https://tutorials.autodesk.io/#provision-access-in-other-products) 11 | - [Node.js](https://nodejs.org) (ideally the _Long Term Support_ version) 12 | - Terminal (for example, [Windows Command Prompt](https://en.wikipedia.org/wiki/Cmd.exe) or [macOS Terminal](https://support.apple.com/guide/terminal/welcome/mac)) 13 | 14 | ### Running locally 15 | 16 | 1. Clone this repository 17 | 2. Install dependencies: `npm install` 18 | 3. Create a _.env_ file in the root folder of this project, and add your APS credentials: 19 | 20 | ``` 21 | APS_CLIENT_ID="your client id" 22 | APS_CLIENT_SECRET="your client secret" 23 | ``` 24 | 25 | 4. Create a new service account: `npx create-service-account ` 26 | - This script will output an email of the newly created service account, and a bunch of environment variables 27 | 5. Add the service account email as a new member to your ACC projects as needed 28 | 6. Add or overwrite the new environment variables in your _.env_ file 29 | 30 | ``` 31 | APS_SA_ID="your service account id" 32 | APS_SA_EMAIL="your service account email" 33 | APS_SA_KEY_ID="your service account key id" 34 | APS_SA_PRIVATE_KEY="your service account private key" 35 | ``` 36 | 37 | 7. Start the server: `npm start` 38 | 8. Open http://localhost:3000, and follow the instructions there 39 | 40 | ## Troubleshooting 41 | 42 | Please contact us via https://aps.autodesk.com/get-help. 43 | 44 | ## License 45 | 46 | This sample is licensed under the terms of the [MIT License](http://opensource.org/licenses/MIT). Please see the [LICENSE](LICENSE) file for more details. 47 | -------------------------------------------------------------------------------- /services/ssa-auth-app/lib/auth.js: -------------------------------------------------------------------------------- 1 | import jwt from "jsonwebtoken"; 2 | 3 | async function _post(endpoint, headers, body) { 4 | const response = await fetch("https://developer.api.autodesk.com" + endpoint, { method: "POST", headers, body }); 5 | if (!response.ok) { 6 | throw new Error(`POST ${endpoint} error: ${response.status} ${response.statusText}\n${await response.text()}`); 7 | } 8 | return response.json(); 9 | } 10 | 11 | /** 12 | * Generates an access token for APS using specific grant type. 13 | * 14 | * @param {string} clientId - The client ID provided by Autodesk. 15 | * @param {string} clientSecret - The client secret provided by Autodesk. 16 | * @param {string} grantType - The grant type for the access token. 17 | * @param {string[]} scopes - An array of scopes for which the token is requested. 18 | * @param {string} [assertion] - The JWT assertion for the access token. 19 | * @returns {Promise<{ access_token: string; token_type: string; expires_in: number; }>} A promise that resolves to the access token response object. 20 | * @throws {Error} If the request for the access token fails. 21 | */ 22 | async function getAccessToken(clientId, clientSecret, grantType, scopes, assertion) { 23 | const headers = { 24 | "Accept": "application/json", 25 | "Authorization": `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString("base64")}`, 26 | "Content-Type": "application/x-www-form-urlencoded" 27 | }; 28 | const body = new URLSearchParams({ 29 | "grant_type": grantType, 30 | "scope": scopes.join(" "), 31 | "assertion": assertion 32 | }); 33 | return _post("/authentication/v2/token", headers, body); 34 | } 35 | 36 | /** 37 | * Creates a JWT assertion for OAuth 2.0 authentication. 38 | * 39 | * @param {string} clientId - The client ID of the application. 40 | * @param {string} serviceAccountId - The service account ID. 41 | * @param {string} serviceAccountKeyId - The key ID of the service account. 42 | * @param {string} serviceAccountPrivateKey - The private key of the service account. 43 | * @param {Array} scopes - The scopes for the access token. 44 | * @returns {string} - The signed JWT assertion. 45 | */ 46 | function createAssertion(clientId, serviceAccountId, serviceAccountKeyId, serviceAccountPrivateKey, scopes) { 47 | const payload = { 48 | iss: clientId, // Issuer 49 | sub: serviceAccountId, // Subject 50 | aud: "https://developer.api.autodesk.com/authentication/v2/token", // Audience 51 | exp: Math.floor(Date.now() / 1000) + 300, // Expiration time (5 minutes from now) 52 | scope: scopes 53 | }; 54 | const options = { 55 | algorithm: "RS256", // Signing algorithm 56 | header: { alg: "RS256", kid: serviceAccountKeyId } // Header with key ID 57 | }; 58 | return jwt.sign(payload, serviceAccountPrivateKey, options); 59 | } 60 | 61 | /** 62 | * Generates an access token for APS using client credentials ("two-legged") flow. 63 | * 64 | * @param {string} clientId - The client ID provided by Autodesk. 65 | * @param {string} clientSecret - The client secret provided by Autodesk. 66 | * @param {string[]} scopes - An array of scopes for which the token is requested. 67 | * @returns {Promise<{ access_token: string; token_type: string; expires_in: number; }>} A promise that resolves to the access token response object. 68 | * @throws {Error} If the request for the access token fails. 69 | */ 70 | export async function getClientCredentialsAccessToken(clientId, clientSecret, scopes) { 71 | return getAccessToken(clientId, clientSecret, "client_credentials", scopes); 72 | } 73 | 74 | /** 75 | * Retrieves an access token for a service account using client credentials and JWT assertion. 76 | * 77 | * @param {string} clientId - The client ID for the OAuth application. 78 | * @param {string} clientSecret - The client secret for the OAuth application. 79 | * @param {string} serviceAccountId - The ID of the service account. 80 | * @param {string} serviceAccountKeyId - The key ID of the service account. 81 | * @param {string} serviceAccountPrivateKey - The private key of the service account. 82 | * @param {string[]} scopes - An array of scopes for the access token. 83 | * @returns {Promise<{ access_token: string; token_type: string; expires_in: number; }>} A promise that resolves to the access token response object. 84 | * @throws {Error} If the access token could not be retrieved. 85 | */ 86 | export async function getServiceAccountAccessToken(clientId, clientSecret, serviceAccountId, serviceAccountKeyId, serviceAccountPrivateKey, scopes) { 87 | const assertion = createAssertion(clientId, serviceAccountId, serviceAccountKeyId, serviceAccountPrivateKey, scopes); 88 | return getAccessToken(clientId, clientSecret, "urn:ietf:params:oauth:grant-type:jwt-bearer", scopes, assertion); 89 | } 90 | 91 | /** 92 | * Creates a new service account with the given name. 93 | * 94 | * @param {string} name - The name of the service account to create (must be between 5 and 64 characters long). 95 | * @param {string} firstName - The first name of the service account user. 96 | * @param {string} lastName - The last name of the service account user. 97 | * @param {string} accessToken - The access token for authentication. 98 | * @returns {Promise<{ serviceAccountId: string; email: string; }>} A promise that resolves to the created service account response. 99 | * @throws {Error} If the request to create the service account fails. 100 | */ 101 | export async function createServiceAccount(name, firstName, lastName, accessToken) { 102 | const headers = { 103 | "Accept": "application/json", 104 | "Authorization": `Bearer ${accessToken}`, 105 | "Content-Type": "application/json" 106 | }; 107 | const body = JSON.stringify({ name, firstName, lastName }); 108 | return _post("/authentication/v2/service-accounts", headers, body); 109 | } 110 | 111 | /** 112 | * Creates a private key for a given service account. 113 | * 114 | * @param {string} serviceAccountId - The ID of the service account for which to create a private key. 115 | * @param {string} accessToken - The access token used for authorization. 116 | * @returns {Promise<{ kid: string; privateKey: string; }>} A promise that resolves to the private key details. 117 | * @throws {Error} If the request to create the private key fails. 118 | */ 119 | export async function createServiceAccountPrivateKey(serviceAccountId, accessToken) { 120 | const headers = { 121 | "Accept": "application/json", 122 | "Authorization": `Bearer ${accessToken}` 123 | }; 124 | return _post(`/authentication/v2/service-accounts/${serviceAccountId}/keys`, headers); 125 | } -------------------------------------------------------------------------------- /services/ssa-auth-app/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ssa-auth-app", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "name": "ssa-auth-app", 8 | "dependencies": { 9 | "@fastify/cors": "^11.0.1", 10 | "dotenv": "^16.4.5", 11 | "fastify": "^5.0.0", 12 | "jsonwebtoken": "^9.0.2" 13 | }, 14 | "bin": { 15 | "create-service-account": "tools/create-service-account.js" 16 | } 17 | }, 18 | "node_modules/@fastify/ajv-compiler": { 19 | "version": "4.0.1", 20 | "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-4.0.1.tgz", 21 | "integrity": "sha512-DxrBdgsjNLP0YM6W5Hd6/Fmj43S8zMKiFJYgi+Ri3htTGAowPVG/tG1wpnWLMjufEnehRivUCKZ1pLDIoZdTuw==", 22 | "dependencies": { 23 | "ajv": "^8.12.0", 24 | "ajv-formats": "^3.0.1", 25 | "fast-uri": "^3.0.0" 26 | } 27 | }, 28 | "node_modules/@fastify/cors": { 29 | "version": "11.0.1", 30 | "resolved": "https://registry.npmjs.org/@fastify/cors/-/cors-11.0.1.tgz", 31 | "integrity": "sha512-dmZaE7M1f4SM8ZZuk5RhSsDJ+ezTgI7v3HHRj8Ow9CneczsPLZV6+2j2uwdaSLn8zhTv6QV0F4ZRcqdalGx1pQ==", 32 | "funding": [ 33 | { 34 | "type": "github", 35 | "url": "https://github.com/sponsors/fastify" 36 | }, 37 | { 38 | "type": "opencollective", 39 | "url": "https://opencollective.com/fastify" 40 | } 41 | ], 42 | "license": "MIT", 43 | "dependencies": { 44 | "fastify-plugin": "^5.0.0", 45 | "toad-cache": "^3.7.0" 46 | } 47 | }, 48 | "node_modules/@fastify/error": { 49 | "version": "4.0.0", 50 | "resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.0.0.tgz", 51 | "integrity": "sha512-OO/SA8As24JtT1usTUTKgGH7uLvhfwZPwlptRi2Dp5P4KKmJI3gvsZ8MIHnNwDs4sLf/aai5LzTyl66xr7qMxA==" 52 | }, 53 | "node_modules/@fastify/fast-json-stringify-compiler": { 54 | "version": "5.0.1", 55 | "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-5.0.1.tgz", 56 | "integrity": "sha512-f2d3JExJgFE3UbdFcpPwqNUEoHWmt8pAKf8f+9YuLESdefA0WgqxeT6DrGL4Yrf/9ihXNSKOqpjEmurV405meA==", 57 | "dependencies": { 58 | "fast-json-stringify": "^6.0.0" 59 | } 60 | }, 61 | "node_modules/@fastify/merge-json-schemas": { 62 | "version": "0.1.1", 63 | "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.1.1.tgz", 64 | "integrity": "sha512-fERDVz7topgNjtXsJTTW1JKLy0rhuLRcquYqNR9rF7OcVpCa2OVW49ZPDIhaRRCaUuvVxI+N416xUoF76HNSXA==", 65 | "dependencies": { 66 | "fast-deep-equal": "^3.1.3" 67 | } 68 | }, 69 | "node_modules/abstract-logging": { 70 | "version": "2.0.1", 71 | "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", 72 | "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==" 73 | }, 74 | "node_modules/ajv": { 75 | "version": "8.17.1", 76 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", 77 | "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", 78 | "dependencies": { 79 | "fast-deep-equal": "^3.1.3", 80 | "fast-uri": "^3.0.1", 81 | "json-schema-traverse": "^1.0.0", 82 | "require-from-string": "^2.0.2" 83 | }, 84 | "funding": { 85 | "type": "github", 86 | "url": "https://github.com/sponsors/epoberezkin" 87 | } 88 | }, 89 | "node_modules/ajv-formats": { 90 | "version": "3.0.1", 91 | "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", 92 | "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", 93 | "dependencies": { 94 | "ajv": "^8.0.0" 95 | }, 96 | "peerDependencies": { 97 | "ajv": "^8.0.0" 98 | }, 99 | "peerDependenciesMeta": { 100 | "ajv": { 101 | "optional": true 102 | } 103 | } 104 | }, 105 | "node_modules/atomic-sleep": { 106 | "version": "1.0.0", 107 | "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", 108 | "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", 109 | "engines": { 110 | "node": ">=8.0.0" 111 | } 112 | }, 113 | "node_modules/avvio": { 114 | "version": "9.1.0", 115 | "resolved": "https://registry.npmjs.org/avvio/-/avvio-9.1.0.tgz", 116 | "integrity": "sha512-fYASnYi600CsH/j9EQov7lECAniYiBFiiAtBNuZYLA2leLe9qOvZzqYHFjtIj6gD2VMoMLP14834LFWvr4IfDw==", 117 | "dependencies": { 118 | "@fastify/error": "^4.0.0", 119 | "fastq": "^1.17.1" 120 | } 121 | }, 122 | "node_modules/buffer-equal-constant-time": { 123 | "version": "1.0.1", 124 | "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", 125 | "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" 126 | }, 127 | "node_modules/cookie": { 128 | "version": "0.7.1", 129 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", 130 | "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", 131 | "engines": { 132 | "node": ">= 0.6" 133 | } 134 | }, 135 | "node_modules/dotenv": { 136 | "version": "16.4.5", 137 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", 138 | "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", 139 | "engines": { 140 | "node": ">=12" 141 | }, 142 | "funding": { 143 | "url": "https://dotenvx.com" 144 | } 145 | }, 146 | "node_modules/ecdsa-sig-formatter": { 147 | "version": "1.0.11", 148 | "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", 149 | "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", 150 | "dependencies": { 151 | "safe-buffer": "^5.0.1" 152 | } 153 | }, 154 | "node_modules/fast-decode-uri-component": { 155 | "version": "1.0.1", 156 | "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", 157 | "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==" 158 | }, 159 | "node_modules/fast-deep-equal": { 160 | "version": "3.1.3", 161 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", 162 | "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" 163 | }, 164 | "node_modules/fast-json-stringify": { 165 | "version": "6.0.0", 166 | "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-6.0.0.tgz", 167 | "integrity": "sha512-FGMKZwniMTgZh7zQp9b6XnBVxUmKVahQLQeRQHqwYmPDqDhcEKZ3BaQsxelFFI5PY7nN71OEeiL47/zUWcYe1A==", 168 | "dependencies": { 169 | "@fastify/merge-json-schemas": "^0.1.1", 170 | "ajv": "^8.12.0", 171 | "ajv-formats": "^3.0.1", 172 | "fast-deep-equal": "^3.1.3", 173 | "fast-uri": "^2.3.0", 174 | "json-schema-ref-resolver": "^1.0.1", 175 | "rfdc": "^1.2.0" 176 | } 177 | }, 178 | "node_modules/fast-json-stringify/node_modules/fast-uri": { 179 | "version": "2.4.0", 180 | "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-2.4.0.tgz", 181 | "integrity": "sha512-ypuAmmMKInk5q7XcepxlnUWDLWv4GFtaJqAzWKqn62IpQ3pejtr5dTVbt3vwqVaMKmkNR55sTT+CqUKIaT21BA==" 182 | }, 183 | "node_modules/fast-querystring": { 184 | "version": "1.1.2", 185 | "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", 186 | "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", 187 | "dependencies": { 188 | "fast-decode-uri-component": "^1.0.1" 189 | } 190 | }, 191 | "node_modules/fast-redact": { 192 | "version": "3.5.0", 193 | "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz", 194 | "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==", 195 | "engines": { 196 | "node": ">=6" 197 | } 198 | }, 199 | "node_modules/fast-uri": { 200 | "version": "3.0.3", 201 | "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", 202 | "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==" 203 | }, 204 | "node_modules/fastify": { 205 | "version": "5.0.0", 206 | "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.0.0.tgz", 207 | "integrity": "sha512-Qe4dU+zGOzg7vXjw4EvcuyIbNnMwTmcuOhlOrOJsgwzvjEZmsM/IeHulgJk+r46STjdJS/ZJbxO8N70ODXDMEQ==", 208 | "funding": [ 209 | { 210 | "type": "github", 211 | "url": "https://github.com/sponsors/fastify" 212 | }, 213 | { 214 | "type": "opencollective", 215 | "url": "https://opencollective.com/fastify" 216 | } 217 | ], 218 | "dependencies": { 219 | "@fastify/ajv-compiler": "^4.0.0", 220 | "@fastify/error": "^4.0.0", 221 | "@fastify/fast-json-stringify-compiler": "^5.0.0", 222 | "abstract-logging": "^2.0.1", 223 | "avvio": "^9.0.0", 224 | "fast-json-stringify": "^6.0.0", 225 | "find-my-way": "^9.0.0", 226 | "light-my-request": "^6.0.0", 227 | "pino": "^9.0.0", 228 | "process-warning": "^4.0.0", 229 | "proxy-addr": "^2.0.7", 230 | "rfdc": "^1.3.1", 231 | "secure-json-parse": "^2.7.0", 232 | "semver": "^7.6.0", 233 | "toad-cache": "^3.7.0" 234 | } 235 | }, 236 | "node_modules/fastify-plugin": { 237 | "version": "5.0.1", 238 | "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-5.0.1.tgz", 239 | "integrity": "sha512-HCxs+YnRaWzCl+cWRYFnHmeRFyR5GVnJTAaCJQiYzQSDwK9MgJdyAsuL3nh0EWRCYMgQ5MeziymvmAhUHYHDUQ==", 240 | "license": "MIT" 241 | }, 242 | "node_modules/fastq": { 243 | "version": "1.17.1", 244 | "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", 245 | "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", 246 | "dependencies": { 247 | "reusify": "^1.0.4" 248 | } 249 | }, 250 | "node_modules/find-my-way": { 251 | "version": "9.1.0", 252 | "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.1.0.tgz", 253 | "integrity": "sha512-Y5jIsuYR4BwWDYYQ2A/RWWE6gD8a0FMgtU+HOq1WKku+Cwdz8M1v8wcAmRXXM1/iqtoqg06v+LjAxMYbCjViMw==", 254 | "dependencies": { 255 | "fast-deep-equal": "^3.1.3", 256 | "fast-querystring": "^1.0.0", 257 | "safe-regex2": "^4.0.0" 258 | }, 259 | "engines": { 260 | "node": ">=14" 261 | } 262 | }, 263 | "node_modules/forwarded": { 264 | "version": "0.2.0", 265 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", 266 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", 267 | "engines": { 268 | "node": ">= 0.6" 269 | } 270 | }, 271 | "node_modules/ipaddr.js": { 272 | "version": "1.9.1", 273 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", 274 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", 275 | "engines": { 276 | "node": ">= 0.10" 277 | } 278 | }, 279 | "node_modules/json-schema-ref-resolver": { 280 | "version": "1.0.1", 281 | "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-1.0.1.tgz", 282 | "integrity": "sha512-EJAj1pgHc1hxF6vo2Z3s69fMjO1INq6eGHXZ8Z6wCQeldCuwxGK9Sxf4/cScGn3FZubCVUehfWtcDM/PLteCQw==", 283 | "dependencies": { 284 | "fast-deep-equal": "^3.1.3" 285 | } 286 | }, 287 | "node_modules/json-schema-traverse": { 288 | "version": "1.0.0", 289 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", 290 | "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" 291 | }, 292 | "node_modules/jsonwebtoken": { 293 | "version": "9.0.2", 294 | "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", 295 | "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", 296 | "dependencies": { 297 | "jws": "^3.2.2", 298 | "lodash.includes": "^4.3.0", 299 | "lodash.isboolean": "^3.0.3", 300 | "lodash.isinteger": "^4.0.4", 301 | "lodash.isnumber": "^3.0.3", 302 | "lodash.isplainobject": "^4.0.6", 303 | "lodash.isstring": "^4.0.1", 304 | "lodash.once": "^4.0.0", 305 | "ms": "^2.1.1", 306 | "semver": "^7.5.4" 307 | }, 308 | "engines": { 309 | "node": ">=12", 310 | "npm": ">=6" 311 | } 312 | }, 313 | "node_modules/jsonwebtoken/node_modules/ms": { 314 | "version": "2.1.3", 315 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 316 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 317 | }, 318 | "node_modules/jwa": { 319 | "version": "1.4.1", 320 | "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", 321 | "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", 322 | "dependencies": { 323 | "buffer-equal-constant-time": "1.0.1", 324 | "ecdsa-sig-formatter": "1.0.11", 325 | "safe-buffer": "^5.0.1" 326 | } 327 | }, 328 | "node_modules/jws": { 329 | "version": "3.2.2", 330 | "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", 331 | "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", 332 | "dependencies": { 333 | "jwa": "^1.4.1", 334 | "safe-buffer": "^5.0.1" 335 | } 336 | }, 337 | "node_modules/light-my-request": { 338 | "version": "6.1.0", 339 | "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.1.0.tgz", 340 | "integrity": "sha512-+NFuhlOGoEwxeQfJ/pobkVFxcnKyDtiX847hLjuB/IzBxIl3q4VJeFI8uRCgb3AlTWL1lgOr+u5+8QdUcr33ng==", 341 | "dependencies": { 342 | "cookie": "^0.7.0", 343 | "process-warning": "^4.0.0", 344 | "set-cookie-parser": "^2.6.0" 345 | } 346 | }, 347 | "node_modules/lodash.includes": { 348 | "version": "4.3.0", 349 | "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", 350 | "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" 351 | }, 352 | "node_modules/lodash.isboolean": { 353 | "version": "3.0.3", 354 | "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", 355 | "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" 356 | }, 357 | "node_modules/lodash.isinteger": { 358 | "version": "4.0.4", 359 | "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", 360 | "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" 361 | }, 362 | "node_modules/lodash.isnumber": { 363 | "version": "3.0.3", 364 | "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", 365 | "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" 366 | }, 367 | "node_modules/lodash.isplainobject": { 368 | "version": "4.0.6", 369 | "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", 370 | "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" 371 | }, 372 | "node_modules/lodash.isstring": { 373 | "version": "4.0.1", 374 | "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", 375 | "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" 376 | }, 377 | "node_modules/lodash.once": { 378 | "version": "4.1.1", 379 | "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", 380 | "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" 381 | }, 382 | "node_modules/on-exit-leak-free": { 383 | "version": "2.1.2", 384 | "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", 385 | "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", 386 | "engines": { 387 | "node": ">=14.0.0" 388 | } 389 | }, 390 | "node_modules/pino": { 391 | "version": "9.5.0", 392 | "resolved": "https://registry.npmjs.org/pino/-/pino-9.5.0.tgz", 393 | "integrity": "sha512-xSEmD4pLnV54t0NOUN16yCl7RIB1c5UUOse5HSyEXtBp+FgFQyPeDutc+Q2ZO7/22vImV7VfEjH/1zV2QuqvYw==", 394 | "dependencies": { 395 | "atomic-sleep": "^1.0.0", 396 | "fast-redact": "^3.1.1", 397 | "on-exit-leak-free": "^2.1.0", 398 | "pino-abstract-transport": "^2.0.0", 399 | "pino-std-serializers": "^7.0.0", 400 | "process-warning": "^4.0.0", 401 | "quick-format-unescaped": "^4.0.3", 402 | "real-require": "^0.2.0", 403 | "safe-stable-stringify": "^2.3.1", 404 | "sonic-boom": "^4.0.1", 405 | "thread-stream": "^3.0.0" 406 | }, 407 | "bin": { 408 | "pino": "bin.js" 409 | } 410 | }, 411 | "node_modules/pino-abstract-transport": { 412 | "version": "2.0.0", 413 | "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", 414 | "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", 415 | "dependencies": { 416 | "split2": "^4.0.0" 417 | } 418 | }, 419 | "node_modules/pino-std-serializers": { 420 | "version": "7.0.0", 421 | "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz", 422 | "integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==" 423 | }, 424 | "node_modules/process-warning": { 425 | "version": "4.0.0", 426 | "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.0.tgz", 427 | "integrity": "sha512-/MyYDxttz7DfGMMHiysAsFE4qF+pQYAA8ziO/3NcRVrQ5fSk+Mns4QZA/oRPFzvcqNoVJXQNWNAsdwBXLUkQKw==" 428 | }, 429 | "node_modules/proxy-addr": { 430 | "version": "2.0.7", 431 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", 432 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", 433 | "dependencies": { 434 | "forwarded": "0.2.0", 435 | "ipaddr.js": "1.9.1" 436 | }, 437 | "engines": { 438 | "node": ">= 0.10" 439 | } 440 | }, 441 | "node_modules/quick-format-unescaped": { 442 | "version": "4.0.4", 443 | "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", 444 | "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==" 445 | }, 446 | "node_modules/real-require": { 447 | "version": "0.2.0", 448 | "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", 449 | "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", 450 | "engines": { 451 | "node": ">= 12.13.0" 452 | } 453 | }, 454 | "node_modules/require-from-string": { 455 | "version": "2.0.2", 456 | "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", 457 | "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", 458 | "engines": { 459 | "node": ">=0.10.0" 460 | } 461 | }, 462 | "node_modules/ret": { 463 | "version": "0.5.0", 464 | "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", 465 | "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==", 466 | "engines": { 467 | "node": ">=10" 468 | } 469 | }, 470 | "node_modules/reusify": { 471 | "version": "1.0.4", 472 | "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", 473 | "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", 474 | "engines": { 475 | "iojs": ">=1.0.0", 476 | "node": ">=0.10.0" 477 | } 478 | }, 479 | "node_modules/rfdc": { 480 | "version": "1.4.1", 481 | "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", 482 | "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==" 483 | }, 484 | "node_modules/safe-buffer": { 485 | "version": "5.2.1", 486 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 487 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 488 | "funding": [ 489 | { 490 | "type": "github", 491 | "url": "https://github.com/sponsors/feross" 492 | }, 493 | { 494 | "type": "patreon", 495 | "url": "https://www.patreon.com/feross" 496 | }, 497 | { 498 | "type": "consulting", 499 | "url": "https://feross.org/support" 500 | } 501 | ] 502 | }, 503 | "node_modules/safe-regex2": { 504 | "version": "4.0.0", 505 | "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-4.0.0.tgz", 506 | "integrity": "sha512-Hvjfv25jPDVr3U+4LDzBuZPPOymELG3PYcSk5hcevooo1yxxamQL/bHs/GrEPGmMoMEwRrHVGiCA1pXi97B8Ew==", 507 | "dependencies": { 508 | "ret": "~0.5.0" 509 | } 510 | }, 511 | "node_modules/safe-stable-stringify": { 512 | "version": "2.5.0", 513 | "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", 514 | "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", 515 | "engines": { 516 | "node": ">=10" 517 | } 518 | }, 519 | "node_modules/secure-json-parse": { 520 | "version": "2.7.0", 521 | "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", 522 | "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==" 523 | }, 524 | "node_modules/semver": { 525 | "version": "7.6.3", 526 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", 527 | "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", 528 | "bin": { 529 | "semver": "bin/semver.js" 530 | }, 531 | "engines": { 532 | "node": ">=10" 533 | } 534 | }, 535 | "node_modules/set-cookie-parser": { 536 | "version": "2.7.1", 537 | "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", 538 | "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==" 539 | }, 540 | "node_modules/sonic-boom": { 541 | "version": "4.2.0", 542 | "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz", 543 | "integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==", 544 | "dependencies": { 545 | "atomic-sleep": "^1.0.0" 546 | } 547 | }, 548 | "node_modules/split2": { 549 | "version": "4.2.0", 550 | "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", 551 | "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", 552 | "engines": { 553 | "node": ">= 10.x" 554 | } 555 | }, 556 | "node_modules/thread-stream": { 557 | "version": "3.1.0", 558 | "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", 559 | "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", 560 | "dependencies": { 561 | "real-require": "^0.2.0" 562 | } 563 | }, 564 | "node_modules/toad-cache": { 565 | "version": "3.7.0", 566 | "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz", 567 | "integrity": "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==", 568 | "engines": { 569 | "node": ">=12" 570 | } 571 | } 572 | } 573 | } 574 | -------------------------------------------------------------------------------- /services/ssa-auth-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ssa-auth-app", 3 | "description": "Simple web application providing authentication for accessing project and design data in Autodesk Construction Cloud using Secure Service Accounts.", 4 | "keywords": [ 5 | "autodesk-platform-services", 6 | "service-account" 7 | ], 8 | "author": "Petr Broz ", 9 | "type": "module", 10 | "bin": { 11 | "create-service-account": "./tools/create-service-account.js" 12 | }, 13 | "scripts": { 14 | "start": "node server.js" 15 | }, 16 | "dependencies": { 17 | "@fastify/cors": "^11.0.1", 18 | "dotenv": "^16.4.5", 19 | "fastify": "^5.0.0", 20 | "jsonwebtoken": "^9.0.2" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /services/ssa-auth-app/server.js: -------------------------------------------------------------------------------- 1 | import dotenv from "dotenv"; 2 | import fastify from "fastify"; 3 | import cors from "@fastify/cors"; 4 | import { getServiceAccountAccessToken } from "./lib/auth.js"; 5 | 6 | const { APS_CLIENT_ID, APS_CLIENT_SECRET, APS_SA_ID, APS_SA_EMAIL, APS_SA_KEY_ID, APS_SA_PRIVATE_KEY, PORT } = dotenv.config().parsed; 7 | if (!APS_CLIENT_ID || !APS_CLIENT_SECRET || !APS_SA_ID || !APS_SA_EMAIL || !APS_SA_KEY_ID || !APS_SA_PRIVATE_KEY) { 8 | console.error("Missing one or more required environment variables: APS_CLIENT_ID, APS_CLIENT_SECRET, APS_SA_ID, APS_SA_EMAIL, APS_SA_KEY_ID, APS_SA_PRIVATE_KEY"); 9 | process.exit(1); 10 | } 11 | const SCOPES = ["viewables:read", "data:read"]; 12 | const HTML = ` 13 | 14 | 15 | 16 | 17 | 18 | SSA Auth App 19 | 20 | 21 |

SSA Auth App

22 |

This is a simple web application providing authentication for accessing project and design data in Autodesk Construction Cloud using Secure Service Accounts.

23 |

Usage

24 |
    25 |
  1. Add the following APS Client ID as a custom integration to your ACC account: ${APS_CLIENT_ID}
  2. 26 |
  3. Invite the following Service Account to your project, and configure its permissions as needed: ${APS_SA_EMAIL}
  4. 27 |
  5. Use the /token endpoint to generate an access token.
  6. 28 |
  7. Use the token to access project or design data in ACC, for example, from a Power BI report.
  8. 29 |
30 | 31 | 32 | `; 33 | 34 | const app = fastify({ logger: true }); 35 | await app.register(cors, { origin: "*", methods: ["GET"] }); 36 | app.get("/", (request, reply) => { reply.type("text/html").send(HTML); }); 37 | app.get("/token", () => getServiceAccountAccessToken(APS_CLIENT_ID, APS_CLIENT_SECRET, APS_SA_ID, APS_SA_KEY_ID, APS_SA_PRIVATE_KEY, SCOPES)); 38 | try { 39 | await app.listen({ port: PORT || 3000 }); 40 | } catch (err) { 41 | app.log.error(err); 42 | process.exit(1); 43 | } -------------------------------------------------------------------------------- /services/ssa-auth-app/tools/create-service-account.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import process from "node:process"; 4 | import dotenv from "dotenv"; 5 | import { getClientCredentialsAccessToken, createServiceAccount, createServiceAccountPrivateKey } from "../lib/auth.js"; 6 | 7 | const { APS_CLIENT_ID, APS_CLIENT_SECRET } = dotenv.config().parsed; 8 | const [,, userName, firstName, lastName] = process.argv; 9 | if (!APS_CLIENT_ID || !APS_CLIENT_SECRET || !userName || !firstName || !lastName) { 10 | console.error("Usage:"); 11 | console.error(" APS_CLIENT_ID= APS_CLIENT_SECRET= node create-service-account.js "); 12 | process.exit(1); 13 | } 14 | 15 | try { 16 | const credentials = await getClientCredentialsAccessToken(APS_CLIENT_ID, APS_CLIENT_SECRET, ["application:service_account:write", "application:service_account_key:write"]); 17 | const { serviceAccountId, email } = await createServiceAccount(userName, firstName, lastName, credentials.access_token); 18 | const { kid, privateKey } = await createServiceAccountPrivateKey(serviceAccountId, credentials.access_token); 19 | console.log("Service account created successfully!"); 20 | console.log("Invite the following user to your project:", email); 21 | console.log("Include the following environment variables to your application:"); 22 | console.log(`APS_SA_ID="${serviceAccountId}"`); 23 | console.log(`APS_SA_EMAIL="${email}"`); 24 | console.log(`APS_SA_KEY_ID="${kid}"`); 25 | console.log(`APS_SA_PRIVATE_KEY="${privateKey}"`); 26 | } catch (err) { 27 | console.error(err); 28 | process.exit(1); 29 | } -------------------------------------------------------------------------------- /visuals/aps-viewer-visual/.eslintignore: -------------------------------------------------------------------------------- 1 | assets 2 | style 3 | dist 4 | node_modules 5 | .eslintrc.js -------------------------------------------------------------------------------- /visuals/aps-viewer-visual/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | "browser": true, 4 | "es6": true, 5 | "es2017": true 6 | }, 7 | root: true, 8 | parser: "@typescript-eslint/parser", 9 | parserOptions: { 10 | project: "tsconfig.json", 11 | tsconfigRootDir: ".", 12 | }, 13 | plugins: [ 14 | "powerbi-visuals" 15 | ], 16 | extends: [ 17 | "plugin:powerbi-visuals/recommended" 18 | ], 19 | rules: {} 20 | }; -------------------------------------------------------------------------------- /visuals/aps-viewer-visual/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .tmp 4 | .env 5 | *.log 6 | webpack.statistics.dev.html 7 | webpack.statistics.prod.html 8 | .DS_Store 9 | Thumbs.db 10 | -------------------------------------------------------------------------------- /visuals/aps-viewer-visual/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0", 3 | "configurations": [ 4 | { 5 | "name": "Debugger", 6 | "type": "chrome", 7 | "request": "attach", 8 | "port": 9222, 9 | "sourceMaps": true, 10 | "webRoot": "${cwd}/" 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /visuals/aps-viewer-visual/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 4, 3 | "editor.insertSpaces": true, 4 | "files.eol": "\n", 5 | "files.watcherExclude": { 6 | "**/.git/objects/**": true, 7 | "**/node_modules/**": true, 8 | ".tmp": true 9 | }, 10 | "files.exclude": { 11 | ".tmp": true 12 | }, 13 | "files.associations": { 14 | "*.resjson": "json" 15 | }, 16 | "search.exclude": { 17 | ".tmp": true, 18 | "typings": true 19 | }, 20 | "json.schemas": [ 21 | { 22 | "fileMatch": [ 23 | "/pbiviz.json" 24 | ], 25 | "url": "./node_modules/powerbi-visuals-api/schema.pbiviz.json" 26 | }, 27 | { 28 | "fileMatch": [ 29 | "/capabilities.json" 30 | ], 31 | "url": "./node_modules/powerbi-visuals-api/schema.capabilities.json" 32 | }, 33 | { 34 | "fileMatch": [ 35 | "/dependencies.json" 36 | ], 37 | "url": "./node_modules/powerbi-visuals-api/schema.dependencies.json" 38 | } 39 | ] 40 | } -------------------------------------------------------------------------------- /visuals/aps-viewer-visual/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 Autodesk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /visuals/aps-viewer-visual/README.md: -------------------------------------------------------------------------------- 1 | # APS Viewer Visual 2 | 3 | [Custom visual](https://powerbi.microsoft.com/en-us/developers/custom-visualization/) for previewing 2D and 3D designs from [Autodesk Platform Services](https://aps.autodesk.com) in Power BI reports. 4 | 5 | ## Usage 6 | 7 | [![Showcase](https://img.youtube.com/vi/8wsA5sd4_Xc/0.jpg)](https://www.youtube.com/watch?v=8wsA5sd4_Xc) 8 | 9 | ## Development 10 | 11 | ### Prerequisites 12 | 13 | - [Set up your environment for developing Power BI visuals](https://learn.microsoft.com/en-us/power-bi/developer/visuals/environment-setup) 14 | - Note: this project has been developed and tested with `pbiviz` version 5.4.x 15 | 16 | The viewer relies on an external web service to generate access tokens for accessing design data in Autodesk Platform Services. The response from the web service should be a JSON with the following structure: 17 | 18 | ```json 19 | { 20 | "access_token": , 21 | "token_type": "Bearer", 22 | "expires_in": 23 | } 24 | ``` 25 | 26 | If you don't want to build your own web service, consider using the [APS Shares App](../../services/aps-shares-app/) application that's part of this repository. 27 | 28 | ### Running locally 29 | 30 | - Clone this repository 31 | - Install dependencies: `npm install` 32 | - Run the local development server: `npm start` 33 | - Open one of your Power BI reports on https://app.powerbi.com 34 | - Add a _Developer Visual_ from the _Visualizations_ tab to the report 35 | 36 | ![Add developer visual](./docs/add-developer-visual.png) 37 | 38 | > If you see an error saying "Can't contact visual server", open a new tab in your browser, navigate to https://localhost:8080/assets, and authorize your browser to use this address. 39 | 40 | - With the visual selected, switch to the _Format your visual_ tab, and add your authentication endpoint URL to the _Access Token Endpoint_ input 41 | 42 | ![Add token endpoint](./docs/add-token-endpoint.png) 43 | 44 | - Drag & drop the columns from your data that represent the design URNs and element IDs to the _Design URNs & Element IDs_ bucket 45 | 46 | ![Add design URNs and element IDs](./docs/add-element-ids.png) 47 | 48 | ### Publish 49 | 50 | - Update [pbiviz.json](./pbiviz.json) with your own visual name, description, etc. 51 | - If needed, update the [capabilities.json](./capabilities.json) file, restricting the websites that the visual will have access to (for example, replacing the `[ "*" ]` list under the `"privileges"` section with `[ "https://your-custom-app.com", "https://*.autodesk.com" ]`) 52 | - Build the *.pbiviz file using `npm run package` 53 | - Import the newly created *.pbiviz file from the _dist_ subfolder into your Power BI report 54 | 55 | ## FAQ 56 | 57 | ### Where do I find URN/GUID values? 58 | 59 | You can retrieve the design URN and viewable GUID after loading the design into any APS-based application. For example, after opening your design in [Autodesk Construction Cloud](https://construction.autodesk.com), open the browser console and type `NOP_VIEWER.model.getData().urn` to retrieve the URN, and `NOP_VIEWER.model.getDocumentNode().guid()` to retrieve the GUID. 60 | 61 | ### Why the visual card cannot load my models? 62 | 63 | Here are check points for your reference: 64 | 65 | 1. Ensure the changes you made has been saved into the PowerBI report after filling in `Access Token Endpoint`, `URN` and `GUID`. Commonly, we can verify this by closing PowerBI Desktop to see if it prompts warnings about unsaved changes. 66 | 67 | 2. Check if you have right access to the model by using the [simple viewer sample](https://aps.autodesk.com/en/docs/viewer/v7/developers_guide/viewer_basics/starting-html/) from APS viewer documentation with the access token and urn from the token endpoint. 68 | 69 | If your models are hosted on BIM360/ACC, please ensure your client id used in the token endpoint has provisioned in the BIM360/ACC account. If not, please follow the [Provision access in other products](https://tutorials.autodesk.io/#provision-access-in-other-products) section in APS tutorial to provision your client id. 70 | 71 | 3. Upload your PowerBI report containing the viewer visual card to your PowerBI workspace (https://app.powerbi.com/groups/me/list?experience=power-bi) and check if there is any error message appearing in the [Web browser dev console](https://developer.chrome.com/docs/devtools/console/). 72 | 73 | ## Troubleshooting 74 | 75 | Please contact us via https://aps.autodesk.com/get-help. 76 | 77 | ## License 78 | 79 | This sample is licensed under the terms of the [MIT License](http://opensource.org/licenses/MIT). Please see the [LICENSE](LICENSE) file for more details. 80 | -------------------------------------------------------------------------------- /visuals/aps-viewer-visual/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autodesk-platform-services/aps-powerbi-tools/3bc6aeb22e20344ba0c865829bd5af8bd39aba8c/visuals/aps-viewer-visual/assets/icon.png -------------------------------------------------------------------------------- /visuals/aps-viewer-visual/capabilities.json: -------------------------------------------------------------------------------- 1 | { 2 | "dataRoles": [ 3 | { 4 | "displayName": "Design URNs & Element IDs", 5 | "name": "elementIds", 6 | "kind": "Grouping" 7 | } 8 | ], 9 | "dataViewMappings": [ 10 | { 11 | "conditions": [ 12 | { 13 | "elementIds": { "min": 1, "max": 2 } 14 | } 15 | ], 16 | "table": { 17 | "rows": { 18 | "select": [ 19 | {"for": { "in": "elementIds" }} 20 | ] 21 | } 22 | } 23 | } 24 | ], 25 | "objects": { 26 | "viewer": { 27 | "properties": { 28 | "accessTokenEndpoint": { 29 | "type": { 30 | "text": true 31 | } 32 | } 33 | } 34 | } 35 | }, 36 | "privileges": [ 37 | { 38 | "name": "WebAccess", 39 | "essential": true, 40 | "parameters": [ 41 | "*" 42 | ] 43 | } 44 | ], 45 | "supportsMultiVisualSelection": true, 46 | "suppressDefaultTitle": true, 47 | "supportsLandingPage": true, 48 | "supportsEmptyDataView": true 49 | } -------------------------------------------------------------------------------- /visuals/aps-viewer-visual/docs/add-developer-visual.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autodesk-platform-services/aps-powerbi-tools/3bc6aeb22e20344ba0c865829bd5af8bd39aba8c/visuals/aps-viewer-visual/docs/add-developer-visual.png -------------------------------------------------------------------------------- /visuals/aps-viewer-visual/docs/add-element-ids.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autodesk-platform-services/aps-powerbi-tools/3bc6aeb22e20344ba0c865829bd5af8bd39aba8c/visuals/aps-viewer-visual/docs/add-element-ids.png -------------------------------------------------------------------------------- /visuals/aps-viewer-visual/docs/add-token-endpoint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autodesk-platform-services/aps-powerbi-tools/3bc6aeb22e20344ba0c865829bd5af8bd39aba8c/visuals/aps-viewer-visual/docs/add-token-endpoint.png -------------------------------------------------------------------------------- /visuals/aps-viewer-visual/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aps-viewer-visual", 3 | "description": "Custom visual for embedding Autodesk Platform Services Viewer into PowerBI reports.", 4 | "license": "MIT", 5 | "scripts": { 6 | "pbiviz": "pbiviz", 7 | "start": "pbiviz start", 8 | "package": "pbiviz package", 9 | "lint": "npx eslint . --ext .js,.jsx,.ts,.tsx" 10 | }, 11 | "dependencies": { 12 | "powerbi-visuals-api": "~5.4.0", 13 | "powerbi-visuals-utils-formattingmodel": "6.0.0" 14 | }, 15 | "devDependencies": { 16 | "@types/forge-viewer": "^7.89.1", 17 | "@typescript-eslint/eslint-plugin": "^5.59.11", 18 | "eslint": "^8.42.0", 19 | "eslint-plugin-powerbi-visuals": "^0.8.1", 20 | "typescript": "4.9.3" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /visuals/aps-viewer-visual/pbiviz.json: -------------------------------------------------------------------------------- 1 | { 2 | "visual": { 3 | "name": "aps-viewer-visual", 4 | "displayName": "APS Viewer Visual (0.0.9.0)", 5 | "guid": "aps_viewer_visual_a4f2990a03324cf79eb44f982719df44", 6 | "visualClassName": "Visual", 7 | "version": "0.0.9.0", 8 | "description": "Visual for previewing shared 2D/3D designs from Autodesk Platform Services.", 9 | "supportUrl": "https://github.com/autodesk-platform-services/aps-powerbi-tools", 10 | "gitHubUrl": "https://github.com/autodesk-platform-services/aps-powerbi-tools" 11 | }, 12 | "apiVersion": "5.4.0", 13 | "author": { 14 | "name": "Autodesk Platform Services - Developer Advocacy Support", 15 | "email": "aps.help@autodesk.com" 16 | }, 17 | "assets": { 18 | "icon": "assets/icon.png" 19 | }, 20 | "style": "style/visual.less", 21 | "capabilities": "capabilities.json", 22 | "dependencies": null, 23 | "stringResources": [] 24 | } -------------------------------------------------------------------------------- /visuals/aps-viewer-visual/src/settings.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { formattingSettings } from 'powerbi-visuals-utils-formattingmodel'; 4 | 5 | import Card = formattingSettings.SimpleCard; 6 | import Slice = formattingSettings.Slice; 7 | import Model = formattingSettings.Model; 8 | 9 | class ViewerCard extends Card { 10 | accessTokenEndpoint = new formattingSettings.TextInput({ 11 | name: 'accessTokenEndpoint', 12 | displayName: 'Access Token Endpoint', 13 | description: 'URL that the viewer can call to generate access tokens.', 14 | placeholder: '', 15 | value: '' 16 | }); 17 | name: string = 'viewer'; 18 | displayName: string = 'Viewer Runtime'; 19 | slices: Array = [this.accessTokenEndpoint]; 20 | } 21 | 22 | export class VisualSettingsModel extends Model { 23 | viewerCard = new ViewerCard(); 24 | cards = [this.viewerCard]; 25 | } 26 | -------------------------------------------------------------------------------- /visuals/aps-viewer-visual/src/viewer.utils.ts: -------------------------------------------------------------------------------- 1 | /// import * as Autodesk from "@types/forge-viewer"; 2 | 3 | 'use strict'; 4 | 5 | const runtime: { options: Autodesk.Viewing.InitializerOptions; ready: Promise | null } = { 6 | options: {}, 7 | ready: null 8 | }; 9 | 10 | declare global { 11 | interface Window { DISABLE_INDEXED_DB: boolean; } 12 | } 13 | 14 | export function initializeViewerRuntime(options: Autodesk.Viewing.InitializerOptions): Promise { 15 | if (!runtime.ready) { 16 | runtime.options = { ...options }; 17 | runtime.ready = (async function () { 18 | window.DISABLE_INDEXED_DB = true; 19 | 20 | await loadScript('https://developer.api.autodesk.com/modelderivative/v2/viewers/7.*/viewer3D.js'); 21 | await loadStylesheet('https://developer.api.autodesk.com/modelderivative/v2/viewers/7.*/style.css'); 22 | return new Promise((resolve) => Autodesk.Viewing.Initializer(runtime.options, resolve)); 23 | })() as Promise; 24 | } else { 25 | if (['accessToken', 'getAccessToken', 'env', 'api', 'language'].some(prop => options[prop] !== runtime.options[prop])) { 26 | return Promise.reject('Cannot initialize another viewer runtime with different settings.'); 27 | } 28 | } 29 | return runtime.ready; 30 | } 31 | 32 | export function loadModel(viewer: Autodesk.Viewing.Viewer3D, urn: string, guid?: string): Promise { 33 | return new Promise(function (resolve, reject) { 34 | Autodesk.Viewing.Document.load( 35 | 'urn:' + urn, 36 | (doc) => { 37 | const view = guid ? doc.getRoot().findByGuid(guid) : doc.getRoot().getDefaultGeometry(); 38 | viewer.loadDocumentNode(doc, view).then(m => resolve(m)); 39 | }, 40 | (code, message, args) => reject({ code, message, args }) 41 | ); 42 | }); 43 | } 44 | 45 | export function getVisibleNodes(model: Autodesk.Viewing.Model): number[] { 46 | const tree = model.getInstanceTree(); 47 | const dbids: number[] = []; 48 | tree.enumNodeChildren(tree.getRootId(), dbid => { 49 | if (tree.getChildCount(dbid) === 0 && !tree.isNodeHidden(dbid) && !tree.isNodeOff(dbid)) { 50 | dbids.push(dbid); 51 | } 52 | }, true); 53 | return dbids; 54 | } 55 | 56 | /** 57 | * Helper class for mapping between "dbIDs" (sequential numbers assigned to each design element; 58 | * typically used by the Viewer APIs) and "external IDs" (typically based on persistent IDs 59 | * from the authoring application, for example, Revit GUIDs). 60 | */ 61 | export class IdMapping { 62 | private readonly externalIdMappingPromise: Promise<{ [externalId: string]: number; }>; 63 | 64 | constructor(private model: Autodesk.Viewing.Model) { 65 | this.externalIdMappingPromise = new Promise((resolve, reject) => { 66 | model.getExternalIdMapping(resolve, reject); 67 | }); 68 | } 69 | 70 | /** 71 | * Converts external IDs into dbIDs. 72 | * @param externalIds List of external IDs. 73 | * @returns List of corresponding dbIDs. 74 | */ 75 | getDbids(externalIds: string[]): Promise { 76 | return this.externalIdMappingPromise 77 | .then(externalIdMapping => externalIds.map(externalId => externalIdMapping[externalId])); 78 | } 79 | 80 | /** 81 | * Converts dbIDs into external IDs. 82 | * @param dbids List of dbIDs. 83 | * @returns List of corresponding external IDs. 84 | */ 85 | getExternalIds(dbids: number[]): Promise { 86 | return new Promise((resolve, reject) => { 87 | this.model.getBulkProperties(dbids, { propFilter: ['externalId'] }, results => { 88 | resolve(results.map(result => result.externalId)) 89 | }, reject); 90 | }); 91 | } 92 | } 93 | 94 | function loadScript(src: string): Promise { 95 | return new Promise((resolve, reject) => { 96 | const el = document.createElement("script"); 97 | el.onload = () => resolve(); 98 | el.onerror = (err) => reject(err); 99 | el.type = 'application/javascript'; 100 | el.src = src; 101 | document.head.appendChild(el); 102 | }); 103 | } 104 | 105 | function loadStylesheet(href: string): Promise { 106 | return new Promise((resolve, reject) => { 107 | const el = document.createElement('link'); 108 | el.onload = () => resolve(); 109 | el.onerror = (err) => reject(err); 110 | el.rel = 'stylesheet'; 111 | el.href = href; 112 | document.head.appendChild(el); 113 | }); 114 | } 115 | -------------------------------------------------------------------------------- /visuals/aps-viewer-visual/src/visual.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import powerbi from 'powerbi-visuals-api'; 4 | import { FormattingSettingsService } from 'powerbi-visuals-utils-formattingmodel'; 5 | import '../style/visual.less'; 6 | 7 | import VisualConstructorOptions = powerbi.extensibility.visual.VisualConstructorOptions; 8 | import VisualUpdateOptions = powerbi.extensibility.visual.VisualUpdateOptions; 9 | import IVisual = powerbi.extensibility.visual.IVisual; 10 | import IVisualHost = powerbi.extensibility.visual.IVisualHost; 11 | import ISelectionManager = powerbi.extensibility.ISelectionManager; 12 | import DataView = powerbi.DataView; 13 | 14 | import { VisualSettingsModel } from './settings'; 15 | import { initializeViewerRuntime, loadModel, IdMapping } from './viewer.utils'; 16 | 17 | /** 18 | * Custom visual wrapper for the Autodesk Platform Services Viewer. 19 | */ 20 | export class Visual implements IVisual { 21 | // Visual state 22 | private host: IVisualHost; 23 | private container: HTMLElement; 24 | private formattingSettings: VisualSettingsModel; 25 | private formattingSettingsService: FormattingSettingsService; 26 | private currentDataView: DataView = null; 27 | private selectionManager: ISelectionManager = null; 28 | 29 | // Visual inputs 30 | private accessTokenEndpoint: string = ''; 31 | 32 | // Viewer runtime 33 | private viewer: Autodesk.Viewing.GuiViewer3D = null; 34 | private urn: string = ''; 35 | private guid: string = ''; 36 | private externalIds: string[] = []; 37 | private model: Autodesk.Viewing.Model = null; 38 | private idMapping: IdMapping = null; 39 | 40 | /** 41 | * Initializes the viewer visual. 42 | * @param options Additional visual initialization options. 43 | */ 44 | constructor(options: VisualConstructorOptions) { 45 | this.host = options.host; 46 | this.selectionManager = options.host.createSelectionManager(); 47 | this.formattingSettingsService = new FormattingSettingsService(); 48 | this.container = options.element; 49 | this.getAccessToken = this.getAccessToken.bind(this); 50 | this.onPropertiesLoaded = this.onPropertiesLoaded.bind(this); 51 | this.onSelectionChanged = this.onSelectionChanged.bind(this); 52 | } 53 | 54 | /** 55 | * Notifies the viewer visual of an update (data, viewmode, size change). 56 | * @param options Additional visual update options. 57 | */ 58 | public async update(options: VisualUpdateOptions): Promise { 59 | // this.logVisualUpdateOptions(options); 60 | 61 | this.formattingSettings = this.formattingSettingsService.populateFormattingSettingsModel(VisualSettingsModel, options.dataViews[0]); 62 | const { accessTokenEndpoint } = this.formattingSettings.viewerCard; 63 | if (accessTokenEndpoint.value !== this.accessTokenEndpoint) { 64 | this.accessTokenEndpoint = accessTokenEndpoint.value; 65 | if (!this.viewer) { 66 | this.initializeViewer(); 67 | } 68 | } 69 | 70 | this.currentDataView = options.dataViews[0]; 71 | if (this.currentDataView.table?.rows?.length > 0) { 72 | const rows = this.currentDataView.table.rows; 73 | const urns = this.collectDesignUrns(this.currentDataView); 74 | if (urns.length > 1) { 75 | this.showNotification('Multiple design URNs detected. Only the first one will be displayed.'); 76 | } 77 | if (urns[0] !== this.urn) { 78 | this.urn = urns[0]; 79 | this.updateModel(); 80 | } 81 | this.externalIds = rows.map(r => r[1].valueOf() as string); 82 | } else { 83 | this.urn = ''; 84 | this.externalIds = []; 85 | this.updateModel(); 86 | } 87 | 88 | if (this.idMapping) { 89 | const isDataFilterApplied = this.currentDataView.metadata?.isDataFilterApplied; 90 | if (this.externalIds.length > 0 && isDataFilterApplied) { 91 | const dbids = await this.idMapping.getDbids(this.externalIds); 92 | this.viewer.isolate(dbids); 93 | this.viewer.fitToView(dbids); 94 | } else { 95 | this.viewer.isolate(); 96 | this.viewer.fitToView(); 97 | } 98 | } 99 | } 100 | 101 | /** 102 | * Returns properties pane formatting model content hierarchies, properties and latest formatting values, Then populate properties pane. 103 | * This method is called once every time we open properties pane or when the user edit any format property. 104 | */ 105 | public getFormattingModel(): powerbi.visuals.FormattingModel { 106 | return this.formattingSettingsService.buildFormattingModel(this.formattingSettings); 107 | } 108 | 109 | /** 110 | * Displays a notification that will automatically disappear after some time. 111 | * @param content HTML content to display inside the notification. 112 | */ 113 | private showNotification(content: string): void { 114 | let notifications = this.container.querySelector('#notifications'); 115 | if (!notifications) { 116 | notifications = document.createElement('div'); 117 | notifications.id = 'notifications'; 118 | this.container.appendChild(notifications); 119 | } 120 | const notification = document.createElement('div'); 121 | notification.className = 'notification'; 122 | notification.innerText = content; 123 | notifications.appendChild(notification); 124 | setTimeout(() => notifications.removeChild(notification), 5000); 125 | } 126 | 127 | /** 128 | * Initializes the viewer runtime. 129 | */ 130 | private async initializeViewer(): Promise { 131 | try { 132 | await initializeViewerRuntime({ env: 'AutodeskProduction2', api: 'streamingV2', getAccessToken: this.getAccessToken }); 133 | this.container.innerText = ''; 134 | this.viewer = new Autodesk.Viewing.GuiViewer3D(this.container); 135 | this.viewer.start(); 136 | this.viewer.addEventListener(Autodesk.Viewing.OBJECT_TREE_CREATED_EVENT, this.onPropertiesLoaded); 137 | this.viewer.addEventListener(Autodesk.Viewing.SELECTION_CHANGED_EVENT, this.onSelectionChanged); 138 | if (this.urn) { 139 | this.updateModel(); 140 | } 141 | } catch (err) { 142 | this.showNotification('Could not initialize viewer runtime. Please see console for more details.'); 143 | console.error(err); 144 | } 145 | } 146 | 147 | /** 148 | * Retrieves a new access token for the viewer. 149 | * @param callback Callback function to call with new access token. 150 | */ 151 | private async getAccessToken(callback: (accessToken: string, expiresIn: number) => void): Promise { 152 | try { 153 | const response = await fetch(this.accessTokenEndpoint); 154 | if (!response.ok) { 155 | throw new Error(await response.text()); 156 | } 157 | const share = await response.json(); 158 | callback(share.access_token, share.expires_in); 159 | } catch (err) { 160 | this.showNotification('Could not retrieve access token. Please see console for more details.'); 161 | console.error(err); 162 | } 163 | } 164 | 165 | /** 166 | * Ensures that the correct model is loaded into the viewer. 167 | */ 168 | private async updateModel(): Promise { 169 | if (!this.viewer) { 170 | return; 171 | } 172 | 173 | if (this.model && this.model.getData().urn !== this.urn) { 174 | this.viewer.unloadModel(this.model); 175 | this.model = null; 176 | this.idMapping = null; 177 | } 178 | 179 | try { 180 | if (this.urn) { 181 | this.model = await loadModel(this.viewer, this.urn, this.guid); 182 | } 183 | } catch (err) { 184 | this.showNotification('Could not load model in the viewer. See console for more details.'); 185 | console.error(err); 186 | } 187 | } 188 | 189 | private async onPropertiesLoaded() { 190 | this.idMapping = new IdMapping(this.model); 191 | } 192 | 193 | private async onSelectionChanged() { 194 | const allExternalIds = this.currentDataView.table.rows; 195 | if (!allExternalIds) { 196 | return; 197 | } 198 | const selectedDbids = this.viewer.getSelection(); 199 | const selectedExternalIds = await this.idMapping.getExternalIds(selectedDbids); 200 | const selectionIds: powerbi.extensibility.ISelectionId[] = []; 201 | for (const selectedExternalId of selectedExternalIds) { 202 | const rowIndex = allExternalIds.findIndex(row => row[1] === selectedExternalId); 203 | if (rowIndex !== -1) { 204 | const selectionId = this.host.createSelectionIdBuilder() 205 | .withTable(this.currentDataView.table, rowIndex) 206 | .createSelectionId(); 207 | selectionIds.push(selectionId); 208 | } 209 | } 210 | this.selectionManager.select(selectionIds); 211 | } 212 | 213 | private collectDesignUrns(dataView: DataView): string[] { 214 | let urns = new Set(dataView.table.rows.map(row => row[0].valueOf() as string)); 215 | return [...urns.values()]; 216 | } 217 | 218 | private logVisualUpdateOptions(options: VisualUpdateOptions) { 219 | const EditMode = { 220 | [powerbi.EditMode.Advanced]: 'Advanced', 221 | [powerbi.EditMode.Default]: 'Default', 222 | }; 223 | const VisualDataChangeOperationKind = { 224 | [powerbi.VisualDataChangeOperationKind.Append]: 'Append', 225 | [powerbi.VisualDataChangeOperationKind.Create]: 'Create', 226 | [powerbi.VisualDataChangeOperationKind.Segment]: 'Segment', 227 | }; 228 | const VisualUpdateType = { 229 | [powerbi.VisualUpdateType.All]: 'All', 230 | [powerbi.VisualUpdateType.Data]: 'Data', 231 | [powerbi.VisualUpdateType.Resize]: 'Resize', 232 | [powerbi.VisualUpdateType.ResizeEnd]: 'ResizeEnd', 233 | [powerbi.VisualUpdateType.Style]: 'Style', 234 | [powerbi.VisualUpdateType.ViewMode]: 'ViewMode', 235 | }; 236 | const ViewMode = { 237 | [powerbi.ViewMode.Edit]: 'Edit', 238 | [powerbi.ViewMode.InFocusEdit]: 'InFocusEdit', 239 | [powerbi.ViewMode.View]: 'View', 240 | }; 241 | console.debug('editMode', EditMode[options.editMode]); 242 | console.debug('isInFocus', options.isInFocus); 243 | console.debug('jsonFilters', options.jsonFilters); 244 | console.debug('operationKind', VisualDataChangeOperationKind[options.operationKind]); 245 | console.debug('type', VisualUpdateType[options.type]); 246 | console.debug('viewMode', ViewMode[options.viewMode]); 247 | console.debug('viewport', options.viewport); 248 | console.debug('Data views:'); 249 | for (const dataView of options.dataViews) { 250 | console.debug(dataView); 251 | } 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /visuals/aps-viewer-visual/style/visual.less: -------------------------------------------------------------------------------- 1 | #overlay { 2 | display: flex; 3 | flex-flow: column; 4 | align-items: center; 5 | justify-content: center; 6 | width: 100%; 7 | height: 100%; 8 | padding: 1em; 9 | background: #fafafa; 10 | } 11 | 12 | #overlay, button { 13 | font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; 14 | } 15 | 16 | #notifications { 17 | z-index: 999; 18 | position: absolute; 19 | inset: 0; 20 | display: flex; 21 | flex-flow: column; 22 | overflow-y: scroll; 23 | } 24 | 25 | .notification { 26 | margin: 1em 1em 0 1em; 27 | padding: 1em; 28 | background: rgba(255, 255, 255, 0.9); 29 | box-shadow: 0 0 3px rgba(0, 0, 0, 0.5); 30 | } 31 | -------------------------------------------------------------------------------- /visuals/aps-viewer-visual/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": false, 4 | "emitDecoratorMetadata": true, 5 | "experimentalDecorators": true, 6 | "target": "es2022", 7 | "sourceMap": true, 8 | "outDir": "./.tmp/build/", 9 | "moduleResolution": "node", 10 | "declaration": true, 11 | "lib": [ 12 | "es2022", 13 | "dom" 14 | ] 15 | }, 16 | "files": [ 17 | "./src/visual.ts" 18 | ] 19 | } -------------------------------------------------------------------------------- /visuals/aps-viewer-visual/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "class-name": true, 4 | "comment-format": [ 5 | true, 6 | "check-space" 7 | ], 8 | "indent": [ 9 | true, 10 | "spaces" 11 | ], 12 | "no-duplicate-variable": true, 13 | "no-eval": true, 14 | "no-internal-module": false, 15 | "no-trailing-whitespace": true, 16 | "no-unsafe-finally": true, 17 | "no-var-keyword": true, 18 | "one-line": [ 19 | true, 20 | "check-open-brace", 21 | "check-whitespace" 22 | ], 23 | "quotemark": [ 24 | false, 25 | "double" 26 | ], 27 | "semicolon": [ 28 | true, 29 | "always" 30 | ], 31 | "triple-equals": [ 32 | true, 33 | "allow-null-check" 34 | ], 35 | "typedef-whitespace": [ 36 | true, 37 | { 38 | "call-signature": "nospace", 39 | "index-signature": "nospace", 40 | "parameter": "nospace", 41 | "property-declaration": "nospace", 42 | "variable-declaration": "nospace" 43 | } 44 | ], 45 | "variable-name": [ 46 | true, 47 | "ban-keywords" 48 | ], 49 | "whitespace": [ 50 | true, 51 | "check-branch", 52 | "check-decl", 53 | "check-operator", 54 | "check-separator", 55 | "check-type" 56 | ], 57 | "insecure-random": true, 58 | "no-banned-terms": true, 59 | "no-cookies": true, 60 | "no-delete-expression": true, 61 | "no-disable-auto-sanitization": true, 62 | "no-document-domain": true, 63 | "no-document-write": true, 64 | "no-exec-script": true, 65 | "no-function-constructor-with-string-args": true, 66 | "no-http-string": [true, "http://www.example.com/?.*", "http://www.examples.com/?.*"], 67 | "no-inner-html": true, 68 | "no-octal-literal": true, 69 | "no-reserved-keywords": true, 70 | "no-string-based-set-immediate": true, 71 | "no-string-based-set-interval": true, 72 | "no-string-based-set-timeout": true, 73 | "non-literal-require": true, 74 | "possible-timing-attack": true, 75 | "react-anchor-blank-noopener": true, 76 | "react-iframe-missing-sandbox": true, 77 | "react-no-dangerous-html": true 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /visuals/tandem-viewer-visual/release/tandem-visual-visual.1.0.0.1.pbiviz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autodesk-platform-services/aps-powerbi-tools/3bc6aeb22e20344ba0c865829bd5af8bd39aba8c/visuals/tandem-viewer-visual/release/tandem-visual-visual.1.0.0.1.pbiviz --------------------------------------------------------------------------------