├── app.json ├── .DS_Store ├── thumbnail.png ├── wwwroot ├── .DS_Store ├── main.js ├── viewer.js ├── index.html ├── Hub │ └── DBHub.js ├── main.css ├── sidebar.js ├── Extensions │ └── DBPropertiesExtension.js └── signalr │ └── dist │ └── browser │ └── signalr.min.js ├── README ├── READFROMDB.png └── UPDATEDBDATA.png ├── appsettings.json ├── libman.json ├── Models ├── DBService.cs ├── APSService.Deriv.cs ├── APSService.cs ├── APSService.Oss.cs ├── APSService.Hubs.cs ├── APSService.Auth.cs └── DBService.MongoDB.cs ├── Program.cs ├── Properties └── launchSettings.json ├── forge-db-sample.csproj ├── Hub └── DBHub.cs ├── LICENSE ├── Controllers ├── ModelsController.cs ├── HubsController.cs ├── DBController.cs └── AuthController.cs ├── Startup.cs ├── README.md └── .gitignore /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "stack": "heroku-18" 3 | } -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autodesk-platform-services/aps-db-sample/HEAD/.DS_Store -------------------------------------------------------------------------------- /thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autodesk-platform-services/aps-db-sample/HEAD/thumbnail.png -------------------------------------------------------------------------------- /wwwroot/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autodesk-platform-services/aps-db-sample/HEAD/wwwroot/.DS_Store -------------------------------------------------------------------------------- /README/READFROMDB.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autodesk-platform-services/aps-db-sample/HEAD/README/READFROMDB.png -------------------------------------------------------------------------------- /README/UPDATEDBDATA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autodesk-platform-services/aps-db-sample/HEAD/README/UPDATEDBDATA.png -------------------------------------------------------------------------------- /appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /libman.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0", 3 | "defaultProvider": "unpkg", 4 | "libraries": [ 5 | { 6 | "library": "@microsoft/signalr@6.0.6", 7 | "destination": "wwwroot/signalr", 8 | "files": [ 9 | "dist/browser/signalr.js", 10 | "dist/browser/signalr.min.js" 11 | ] 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /Models/DBService.cs: -------------------------------------------------------------------------------- 1 | public partial class DBService 2 | { 3 | private readonly string _mongodbdatabaseName; 4 | private readonly string _mongodbcollection; 5 | private readonly string _mongodbdatabaseConnectionString; 6 | private readonly string _properties; 7 | 8 | public DBService(string databaseName, string collectionName, string databaseConnectionString, string properties) 9 | { 10 | _mongodbdatabaseName = databaseName; 11 | _mongodbcollection = collectionName; 12 | _mongodbdatabaseConnectionString = databaseConnectionString; 13 | _properties = properties; 14 | } 15 | 16 | } -------------------------------------------------------------------------------- /Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.Extensions.Hosting; 3 | using aps_viewer_db_properties.Hubs; 4 | using Microsoft.AspNetCore.SignalR; 5 | 6 | public class Program 7 | { 8 | public static void Main(string[] args) 9 | { 10 | var host = CreateHostBuilder(args).Build(); 11 | var hubContext = host.Services.GetService(typeof(IHubContext)); 12 | host.Run(); 13 | } 14 | 15 | public static IHostBuilder CreateHostBuilder(string[] args) => 16 | Host.CreateDefaultBuilder(args) 17 | .ConfigureWebHostDefaults(webBuilder => 18 | { 19 | webBuilder.UseStartup(); 20 | }); 21 | } -------------------------------------------------------------------------------- /Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:20858", 7 | "sslPort": 44363 8 | } 9 | }, 10 | "profiles": { 11 | "aps_db_sample": { 12 | "commandName": "Project", 13 | "dotnetRunMessages": true, 14 | "launchBrowser": true, 15 | "applicationUrl": "http://localhost:8080", 16 | "environmentVariables": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | } 19 | }, 20 | "IIS Express": { 21 | "commandName": "IISExpress", 22 | "launchBrowser": true, 23 | "environmentVariables": { 24 | "ASPNETCORE_ENVIRONMENT": "Development" 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /forge-db-sample.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | disable 7 | aps_db_sample 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /Models/APSService.Deriv.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | using Autodesk.ModelDerivative; 4 | using Autodesk.ModelDerivative.Model; 5 | 6 | public record TranslationStatus(string Status, string Progress, IEnumerable? Messages); 7 | 8 | public partial class APSService 9 | { 10 | public static string Base64Encode(string plainText) 11 | { 12 | var plainTextBytes = System.Text.Encoding.UTF8.GetBytes(plainText); 13 | return System.Convert.ToBase64String(plainTextBytes).TrimEnd('='); 14 | } 15 | 16 | public async Task GetTranslationStatus(string urn) 17 | { 18 | var token = await GetInternalToken(); 19 | Manifest manifest = await _modelDerivativeClient.GetManifestAsync(urn, Autodesk.ModelDerivative.Model.Region.US, accessToken: token.InternalToken); 20 | var messages = new List(); 21 | return new TranslationStatus(manifest.Status, manifest.Status, messages); 22 | } 23 | } -------------------------------------------------------------------------------- /Hub/DBHub.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.SignalR; 6 | using MongoDB.Driver; 7 | 8 | namespace aps_viewer_db_properties.Hubs 9 | { 10 | public class DBHub : Microsoft.AspNetCore.SignalR.Hub 11 | { 12 | public async static Task SendData(IHubContext hub, string connectionId, string selecteddbId, Dictionary properties) 13 | { 14 | await hub.Clients.Client(connectionId).SendAsync("ReceiveProperties", selecteddbId, properties); 15 | } 16 | 17 | public async static Task SendUpdate(IHubContext hub, string connectionId, string selecteddbId, bool updateResult, string message, Dictionary properties, string urn) 18 | { 19 | await hub.Clients.Client(connectionId).SendAsync("ReceiveUpdate", selecteddbId, updateResult, message); 20 | if (updateResult) 21 | { 22 | await hub.Clients.AllExcept(connectionId).SendAsync("ReceiveModification", selecteddbId, properties, urn); 23 | } 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 Autodesk Platform Services 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 | -------------------------------------------------------------------------------- /wwwroot/main.js: -------------------------------------------------------------------------------- 1 | import { initViewer, loadModel } from './viewer.js'; 2 | import { initHubsTree, initOssTree } from './sidebar.js'; 3 | 4 | const login = document.getElementById('login'); 5 | try { 6 | const viewer = await initViewer(document.getElementById('preview')); 7 | initOssTree('#osstree', (id) => loadModel(viewer, id)); 8 | const resp = await fetch('/api/auth/profile'); 9 | if (resp.ok) { 10 | const user = await resp.json(); 11 | login.innerText = `Logout (${user.name})`; 12 | login.onclick = () => { 13 | // Log the user out (see https://aps.autodesk.com/blog/log-out-forge) 14 | const iframe = document.createElement('iframe'); 15 | iframe.style.visibility = 'hidden'; 16 | iframe.src = 'https://accounts.autodesk.com/Authentication/LogOut'; 17 | document.body.appendChild(iframe); 18 | iframe.onload = () => { 19 | window.location.replace('/api/auth/logout'); 20 | document.body.removeChild(iframe); 21 | }; 22 | } 23 | /*const viewer = await initViewer(document.getElementById('preview'));*/ 24 | initHubsTree('#hubstree', (id) => loadModel(viewer, window.btoa(id).replace(/=/g, ''))); 25 | } else { 26 | login.innerText = 'Login'; 27 | login.onclick = () => window.location.replace('/api/auth/login'); 28 | } 29 | login.style.visibility = 'visible'; 30 | } catch (err) { 31 | alert('Could not initialize the application. See console for more details.'); 32 | console.error(err); 33 | } -------------------------------------------------------------------------------- /wwwroot/viewer.js: -------------------------------------------------------------------------------- 1 | 2 | async function getAccessToken(callback) { 3 | try { 4 | const resp = await fetch('/api/auth/token'); 5 | if (!resp.ok) 6 | throw new Error(await resp.text()); 7 | const { access_token, expires_in } = await resp.json(); 8 | callback(access_token, expires_in); 9 | } catch (err) { 10 | alert('Could not obtain access token. See the console for more details.'); 11 | console.error(err); 12 | } 13 | } 14 | 15 | export function initViewer(container) { 16 | return new Promise(function (resolve, reject) { 17 | Autodesk.Viewing.Initializer({ getAccessToken }, async function () { 18 | const config = { 19 | extensions: ['Autodesk.DocumentBrowser', 'Autodesk.VisualClusters'] 20 | }; 21 | const viewer = new Autodesk.Viewing.GuiViewer3D(container, config); 22 | viewer.addEventListener(Autodesk.Viewing.GEOMETRY_LOADED_EVENT, () => viewer.loadExtension('DBPropertiesExtension', { "properties": {} })); 23 | viewer.start(); 24 | viewer.setTheme('light-theme'); 25 | resolve(viewer); 26 | }); 27 | }); 28 | } 29 | 30 | export function loadModel(viewer, urn) { 31 | function onDocumentLoadSuccess(doc) { 32 | viewer.loadDocumentNode(doc, doc.getRoot().getDefaultGeometry()); 33 | } 34 | function onDocumentLoadFailure(code, message) { 35 | alert('Could not load model. See console for more details.'); 36 | console.error(message); 37 | } 38 | Autodesk.Viewing.Document.load('urn:' + urn, onDocumentLoadSuccess, onDocumentLoadFailure); 39 | } -------------------------------------------------------------------------------- /Controllers/ModelsController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Http; 7 | using Microsoft.AspNetCore.Mvc; 8 | using Newtonsoft.Json; 9 | using Newtonsoft.Json.Linq; 10 | 11 | [ApiController] 12 | [Route("api/[controller]")] 13 | public class ModelsController : ControllerBase 14 | { 15 | public record BucketObject(string name, string urn); 16 | 17 | private readonly APSService _APSService; 18 | 19 | public ModelsController(APSService APSService) 20 | { 21 | _APSService = APSService; 22 | } 23 | 24 | [HttpGet()] 25 | public async Task> GetModels() 26 | { 27 | var objects = await _APSService.GetObjects(); 28 | return from o in objects 29 | select new BucketObject(o.ObjectKey, APSService.Base64Encode(o.ObjectId)); 30 | } 31 | 32 | [HttpGet("bucket")] 33 | public async Task GetBucketName() 34 | { 35 | string bucketKey = await _APSService.GetBucketKey(); 36 | dynamic bucket = new JObject(); 37 | bucket.name = bucketKey; 38 | return JsonConvert.SerializeObject(bucket); 39 | } 40 | 41 | [HttpGet("{urn}/status")] 42 | public async Task GetModelStatus(string urn) 43 | { 44 | try 45 | { 46 | var status = await _APSService.GetTranslationStatus(urn); 47 | return status; 48 | } 49 | catch (Exception ex) 50 | { 51 | return new TranslationStatus("N/A", ex.Message, null); 52 | } 53 | } 54 | 55 | } -------------------------------------------------------------------------------- /Models/APSService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Autodesk.Authentication; 4 | using Autodesk.Authentication.Model; 5 | using Autodesk.DataManagement; 6 | using Autodesk.ModelDerivative; 7 | using Autodesk.Oss; 8 | using Autodesk.SDKManager; 9 | 10 | public class Tokens 11 | { 12 | public string InternalToken; 13 | public string PublicToken; 14 | public string RefreshToken; 15 | public DateTime ExpiresAt; 16 | } 17 | 18 | public partial class APSService 19 | { 20 | private readonly string _clientId; 21 | private readonly string _clientSecret; 22 | private readonly string _callbackUri; 23 | private readonly string _bucket; 24 | private readonly AuthenticationClient _authClient; 25 | private readonly DataManagementClient _dataManagementClient; 26 | private readonly ModelDerivativeClient _modelDerivativeClient; 27 | private readonly OssClient _ossClient; 28 | private readonly List InternalTokenScopes = new List { Scopes.DataRead, Scopes.ViewablesRead }; 29 | private readonly List PublicTokenScopes = new List { Scopes.DataRead, Scopes.ViewablesRead }; 30 | 31 | public APSService(string clientId, string clientSecret, string callbackUri, string bucket = null) 32 | { 33 | _clientId = clientId; 34 | _clientSecret = clientSecret; 35 | _callbackUri = callbackUri; 36 | _bucket = string.IsNullOrEmpty(bucket) ? string.Format("{0}-basic-app", _clientId.ToLower()) : bucket; 37 | SDKManager sdkManager = SdkManagerBuilder 38 | .Create() // Creates SDK Manager Builder itself. 39 | .Build(); 40 | _authClient = new AuthenticationClient(sdkManager); 41 | _dataManagementClient = new DataManagementClient(sdkManager); 42 | _modelDerivativeClient = new ModelDerivativeClient(sdkManager); 43 | _ossClient = new OssClient(sdkManager); 44 | } 45 | } -------------------------------------------------------------------------------- /Models/APSService.Oss.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net; 4 | using System.Threading.Tasks; 5 | using Autodesk.Oss; 6 | using Autodesk.Oss.Model; 7 | 8 | public partial class APSService 9 | { 10 | private async Task EnsureBucketExists(string bucketKey) 11 | { 12 | const string region = "US"; 13 | var token = await GetInternalToken(); 14 | try 15 | { 16 | await _ossClient.GetBucketDetailsAsync(bucketKey, accessToken: token.InternalToken); 17 | } 18 | catch (OssApiException e) 19 | { 20 | if (e.StatusCode == HttpStatusCode.NotFound) 21 | { 22 | var payload = new CreateBucketsPayload 23 | { 24 | BucketKey = bucketKey, 25 | PolicyKey = "persistent" 26 | }; 27 | await _ossClient.CreateBucketAsync(region, payload, token.InternalToken); 28 | } 29 | else 30 | { 31 | throw e; 32 | } 33 | } 34 | } 35 | 36 | public async Task> GetObjects() 37 | { 38 | const int PageSize = 64; 39 | await EnsureBucketExists(_bucket); 40 | var token = await GetInternalToken(); 41 | var results = new List(); 42 | var response = await _ossClient.GetObjectsAsync(_bucket, PageSize, accessToken: token.InternalToken); 43 | results.AddRange(response.Items); 44 | while (!string.IsNullOrEmpty(response.Next)) 45 | { 46 | var queryParams = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(new Uri(response.Next).Query); 47 | response = await _ossClient.GetObjectsAsync(_bucket, PageSize, null, queryParams["startAt"], accessToken: token.InternalToken); 48 | results.AddRange(response.Items); 49 | } 50 | return results; 51 | } 52 | 53 | public async Task GetBucketKey() 54 | { 55 | await EnsureBucketExists(_bucket); 56 | return _bucket; 57 | } 58 | } -------------------------------------------------------------------------------- /wwwroot/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Autodesk Platform Services: DB Properties 12 | 13 | 14 | 15 | 20 | 24 |
25 |
26 |
Gathering Data!
27 |
Success!
28 |
Fail!
29 |
30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /wwwroot/Hub/DBHub.js: -------------------------------------------------------------------------------- 1 | ///////////////////////////////////////////////////////////////////// 2 | // Copyright (c) Autodesk, Inc. All rights reserved 3 | // Written by APS Partner Development 4 | // 5 | // Permission to use, copy, modify, and distribute this software in 6 | // object code form for any purpose and without fee is hereby granted, 7 | // provided that the above copyright notice appears in all copies and 8 | // that both that copyright notice and the limited warranty and 9 | // restricted rights notice below appear in all supporting 10 | // documentation. 11 | // 12 | // AUTODESK PROVIDES THIS PROGRAM "AS IS" AND WITH ALL FAULTS. 13 | // AUTODESK SPECIFICALLY DISCLAIMS ANY IMPLIED WARRANTY OF 14 | // MERCHANTABILITY OR FITNESS FOR A PARTICULAR USE. AUTODESK, INC. 15 | // DOES NOT WARRANT THAT THE OPERATION OF THE PROGRAM WILL BE 16 | // UNINTERRUPTED OR ERROR FREE. 17 | ///////////////////////////////////////////////////////////////////// 18 | 19 | var connection = new signalR.HubConnectionBuilder().withUrl("/dbhub").build(); 20 | 21 | connection.on("ReceiveProperties", function (selecteddbId, properties) { 22 | addProperties(selecteddbId, properties).then(() => { 23 | $("div.ready").fadeIn(500).delay(1500).fadeOut(500); 24 | }).catch((err) => { 25 | console.log(err); 26 | $("div.failure").fadeIn(500).delay(1500).fadeOut(500); 27 | }); 28 | }); 29 | 30 | connection.on("ReceiveUpdate", function (selecteddbId, updateResult, message) { 31 | showUpdateResult(selecteddbId, updateResult); 32 | console.log(message); 33 | }); 34 | 35 | connection.on("ReceiveModification", function (selecteddbId, properties, urn) { 36 | //let disableNotification = $('#disablenotifications')[0].checked; 37 | if (urn.replaceAll('=', '') === _viewer.model.getSeedUrn() /*&& !disableNotification*/) { 38 | addProperties(selecteddbId, properties); 39 | showNotification(selecteddbId); 40 | } 41 | }); 42 | 43 | connection.start().then(function () { 44 | //No function for now 45 | }).catch(function (err) { 46 | return console.error(err.toString()); 47 | }); -------------------------------------------------------------------------------- /Controllers/HubsController.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.Extensions.Logging; 4 | using Newtonsoft.Json; 5 | 6 | [ApiController] 7 | [Route("api/[controller]")] 8 | public class HubsController : ControllerBase 9 | { 10 | private readonly ILogger _logger; 11 | private readonly APSService _APSService; 12 | 13 | public HubsController(ILogger logger, APSService APSService) 14 | { 15 | _logger = logger; 16 | _APSService = APSService; 17 | } 18 | 19 | [HttpGet()] 20 | public async Task> ListHubs() 21 | { 22 | var tokens = await AuthController.PrepareTokens(Request, Response, _APSService); 23 | if (tokens == null) 24 | { 25 | return Unauthorized(); 26 | } 27 | var hubs = await _APSService.GetHubs(tokens); 28 | return JsonConvert.SerializeObject(hubs); 29 | } 30 | 31 | [HttpGet("{hub}/projects")] 32 | public async Task> ListProjects(string hub) 33 | { 34 | var tokens = await AuthController.PrepareTokens(Request, Response, _APSService); 35 | if (tokens == null) 36 | { 37 | return Unauthorized(); 38 | } 39 | var projects = await _APSService.GetProjects(hub, tokens); 40 | return JsonConvert.SerializeObject(projects); 41 | } 42 | 43 | [HttpGet("{hub}/projects/{project}/contents")] 44 | public async Task> ListItems(string hub, string project, [FromQuery] string? folder_id) 45 | { 46 | var tokens = await AuthController.PrepareTokens(Request, Response, _APSService); 47 | if (tokens == null) 48 | { 49 | return Unauthorized(); 50 | } 51 | var contents = await _APSService.GetContents(hub, project, folder_id, tokens); 52 | return JsonConvert.SerializeObject(contents); 53 | } 54 | 55 | [HttpGet("{hub}/projects/{project}/contents/{item}/versions")] 56 | public async Task> ListVersions(string hub, string project, string item) 57 | { 58 | var tokens = await AuthController.PrepareTokens(Request, Response, _APSService); 59 | if (tokens == null) 60 | { 61 | return Unauthorized(); 62 | } 63 | var versions = await _APSService.GetVersions(hub, project, item, tokens); 64 | return JsonConvert.SerializeObject(versions); 65 | } 66 | } -------------------------------------------------------------------------------- /Controllers/DBController.cs: -------------------------------------------------------------------------------- 1 | using aps_viewer_db_properties.Hubs; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.AspNetCore.SignalR; 4 | using Microsoft.Extensions.Logging; 5 | using System; 6 | 7 | [ApiController] 8 | [Route("api/[controller]")] 9 | public class DBController : ControllerBase 10 | { 11 | public readonly IHubContext _dbHub; 12 | private readonly ILogger _logger; 13 | private readonly DBService _dbService; 14 | 15 | public DBController(ILogger logger, DBService dbService, IHubContext dbHub) 16 | { 17 | _dbHub = dbHub; 18 | GC.KeepAlive(_dbHub); 19 | _logger = logger; 20 | _dbService = dbService; 21 | } 22 | 23 | [HttpPost("dbconnector")] 24 | public object PostDBData([FromBody] DBUpdate dBUpdate) 25 | { 26 | 27 | switch (dBUpdate.dbProvider.ToLower()) 28 | { 29 | case "mongo": 30 | _dbService.UpdateDataFromMongoDB(dBUpdate.connectionId, dBUpdate.property, dBUpdate.selecteddbId, dBUpdate.itemId, _dbHub); 31 | break; 32 | default: 33 | break; 34 | } 35 | return new { Success = true }; 36 | } 37 | 38 | [HttpGet("dbconnector")] 39 | public object GetDBData() 40 | { 41 | string connectionId = base.Request.Query["connectionId"]; 42 | //string externalId = base.Request.Query["externalId"]; 43 | string selecteddbId = base.Request.Query["selecteddbId"]; 44 | string dbProvider = base.Request.Query["dbProvider"]; 45 | //string projectId = base.Request.Query["projectId"]; 46 | string itemId = base.Request.Query["itemId"]; 47 | 48 | 49 | switch (dbProvider.ToLower()) 50 | { 51 | case "mongo": 52 | _dbService.ExtractDataFromMongoDB(connectionId, selecteddbId, itemId, _dbHub); 53 | break; 54 | default: 55 | break; 56 | } 57 | 58 | return new { Success = true }; 59 | } 60 | 61 | } 62 | 63 | public class DBUpdate 64 | { 65 | public Property property { get; set; } 66 | public string connectionId { get; set; } 67 | public string dbProvider { get; set; } 68 | public string selecteddbId { get; set; } 69 | public string itemId { get; set; } 70 | } 71 | 72 | public class Property 73 | { 74 | public string category { get; set; } 75 | public string name { get; set; } 76 | public string value { get; set; } 77 | } 78 | -------------------------------------------------------------------------------- /Models/APSService.Hubs.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | using Autodesk.DataManagement.Model; 4 | 5 | public partial class APSService 6 | { 7 | public async Task> GetHubs(Tokens tokens) 8 | { 9 | Hubs hubs = await _dataManagementClient.GetHubsAsync(accessToken: tokens.InternalToken); 10 | return hubs.Data; 11 | } 12 | 13 | public async Task> GetProjects(string hubId, Tokens tokens) 14 | { 15 | Projects projects = await _dataManagementClient.GetHubProjectsAsync(hubId, accessToken: tokens.InternalToken); 16 | return projects.Data; 17 | } 18 | 19 | public async Task> GetContents(string hubId, string projectId, string? folderId, Tokens tokens) 20 | { 21 | var contents = new List(); 22 | if (string.IsNullOrEmpty(folderId)) 23 | { 24 | TopFolders topFolders = await _dataManagementClient.GetProjectTopFoldersAsync(hubId, projectId, accessToken: tokens.InternalToken); 25 | foreach (TopFoldersData topFolderData in topFolders.Data) 26 | { 27 | contents.Add(new 28 | { 29 | type = topFolderData.Type, 30 | id = topFolderData.Id, 31 | name = topFolderData.Attributes.DisplayName 32 | }); 33 | } 34 | } 35 | else 36 | { 37 | FolderContents folderContents = await _dataManagementClient.GetFolderContentsAsync(projectId, folderId, accessToken: tokens.InternalToken); 38 | foreach (FolderContentsData folderContentData in folderContents.Data) 39 | { 40 | contents.Add(new 41 | { 42 | type = folderContentData.Type, 43 | id = folderContentData.Id, 44 | name = folderContentData.Attributes.DisplayName 45 | }); 46 | } 47 | } 48 | return contents; 49 | } 50 | 51 | public async Task> GetVersions(string hubId, string projectId, string itemId, Tokens tokens) 52 | { 53 | var versions = new List(); 54 | Versions versionsData = await _dataManagementClient.GetItemVersionsAsync(projectId, itemId, accessToken: tokens.InternalToken); 55 | foreach (VersionsData version in versionsData.Data) 56 | { 57 | versions.Add(new 58 | { 59 | id = version.Id, 60 | createTime = version.Attributes.CreateTime 61 | }); 62 | } 63 | return versions; 64 | } 65 | } -------------------------------------------------------------------------------- /Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using aps_viewer_db_properties.Hubs; 3 | using Microsoft.AspNetCore.Builder; 4 | using Microsoft.AspNetCore.Hosting; 5 | using Microsoft.Extensions.Configuration; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Microsoft.Extensions.Hosting; 8 | 9 | public class Startup 10 | { 11 | public Startup(IConfiguration configuration) 12 | { 13 | Configuration = configuration; 14 | } 15 | 16 | public IConfiguration Configuration { get; } 17 | 18 | // This method gets called by the runtime. Use this method to add services to the container. 19 | public void ConfigureServices(IServiceCollection services) 20 | { 21 | services.AddControllers().SetCompatibilityVersion(Microsoft.AspNetCore.Mvc.CompatibilityVersion.Latest).AddNewtonsoftJson(); 22 | var APSClientID = Configuration["APS_CLIENT_ID"]; 23 | var APSClientSecret = Configuration["APS_CLIENT_SECRET"]; 24 | var APSCallbackURL = Configuration["APS_CALLBACK_URL"]; 25 | var APSBucket = Configuration["APS_BUCKET"]; // Optional 26 | var mongoDBConnectionString = Configuration["MONGODB_CON_STRING"]; 27 | var mongoDBName = Configuration["MONGODB_DBNAME"]; 28 | var mongoDBCollection = Configuration["MONGODB_COLLECTION"]; 29 | var mongoDBProperties = Configuration["DB_PROPERTIES_NAMES"]; 30 | if (string.IsNullOrEmpty(APSClientID) || string.IsNullOrEmpty(APSClientSecret) || string.IsNullOrEmpty(APSCallbackURL)) 31 | { 32 | throw new ApplicationException("Missing required environment variables APS_CLIENT_ID, APS_CLIENT_SECRET, or APS_CALLBACK_URL."); 33 | } 34 | services.AddSingleton(new APSService(APSClientID, APSClientSecret, APSCallbackURL, APSBucket)); 35 | services.AddSingleton(new DBService(mongoDBName, mongoDBCollection, mongoDBConnectionString, mongoDBProperties)); 36 | services.AddSignalR(o => 37 | { 38 | o.EnableDetailedErrors = true; 39 | o.MaximumReceiveMessageSize = 10240; // bytes 40 | }); 41 | } 42 | 43 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 44 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 45 | { 46 | if (env.IsDevelopment()) 47 | { 48 | app.UseDeveloperExceptionPage(); 49 | } 50 | app.UseDefaultFiles(); 51 | app.UseStaticFiles(); 52 | app.UseRouting(); 53 | app.UseEndpoints(endpoints => 54 | { 55 | endpoints.MapControllers(); 56 | endpoints.MapHub("/dbhub"); 57 | }); 58 | } 59 | } -------------------------------------------------------------------------------- /Models/APSService.Auth.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using Autodesk.Authentication.Model; 5 | 6 | public partial class APSService 7 | { 8 | private Tokens _internalTokenCache; 9 | private Tokens _publicTokenCache; 10 | 11 | public string GetAuthorizationURL() 12 | { 13 | return _authClient.Authorize(_clientId, ResponseType.Code, _callbackUri, InternalTokenScopes); 14 | } 15 | 16 | public async Task GenerateTokens(string code) 17 | { 18 | ThreeLeggedToken internalAuth = await _authClient.GetThreeLeggedTokenAsync(_clientId, _clientSecret, code, _callbackUri); 19 | RefreshToken publicAuth = await _authClient.GetRefreshTokenAsync(_clientId, _clientSecret, internalAuth.RefreshToken, PublicTokenScopes); 20 | return new Tokens 21 | { 22 | PublicToken = publicAuth.AccessToken, 23 | InternalToken = internalAuth.AccessToken, 24 | RefreshToken = publicAuth._RefreshToken, 25 | ExpiresAt = DateTime.Now.ToUniversalTime().AddSeconds((double)internalAuth.ExpiresIn) 26 | }; 27 | } 28 | 29 | public async Task RefreshTokens(Tokens tokens) 30 | { 31 | RefreshToken internalAuth = await _authClient.GetRefreshTokenAsync(_clientId, _clientSecret, tokens.RefreshToken, InternalTokenScopes); 32 | RefreshToken publicAuth = await _authClient.GetRefreshTokenAsync(_clientId, _clientSecret, internalAuth._RefreshToken, PublicTokenScopes); 33 | return new Tokens 34 | { 35 | PublicToken = publicAuth.AccessToken, 36 | InternalToken = internalAuth.AccessToken, 37 | RefreshToken = publicAuth._RefreshToken, 38 | ExpiresAt = DateTime.Now.ToUniversalTime().AddSeconds((double)internalAuth.ExpiresIn).AddSeconds(-1700) 39 | }; 40 | } 41 | 42 | public async Task GetUserProfile(Tokens tokens) 43 | { 44 | var userInfo = await _authClient.GetUserInfoAsync(tokens.InternalToken); 45 | return userInfo; 46 | } 47 | 48 | private async Task GetToken(List scopes) 49 | { 50 | TwoLeggedToken auth = await _authClient.GetTwoLeggedTokenAsync(_clientId, _clientSecret, scopes); 51 | return new Tokens 52 | { 53 | PublicToken = auth.AccessToken, 54 | InternalToken = auth.AccessToken, 55 | RefreshToken = null, 56 | ExpiresAt = DateTime.UtcNow.AddSeconds((double)auth.ExpiresIn) 57 | }; 58 | } 59 | 60 | private async Task GetInternalToken() 61 | { 62 | if (_internalTokenCache == null || _internalTokenCache.ExpiresAt < DateTime.UtcNow) 63 | _internalTokenCache = await GetToken(new List { Scopes.BucketCreate, Scopes.BucketRead, Scopes.DataRead, Scopes.DataWrite, Scopes.DataCreate }); 64 | return _internalTokenCache; 65 | } 66 | 67 | public async Task GetPublicToken() 68 | { 69 | if (_publicTokenCache == null || _publicTokenCache.ExpiresAt < DateTime.UtcNow) 70 | _publicTokenCache = await GetToken(new List { Scopes.ViewablesRead }); 71 | return _publicTokenCache; 72 | } 73 | } -------------------------------------------------------------------------------- /wwwroot/main.css: -------------------------------------------------------------------------------- 1 | body, html { 2 | margin: 0; 3 | padding: 0; 4 | height: 100vh; 5 | font-family: ArtifaktElement; 6 | } 7 | 8 | #header, #sidebar, #preview { 9 | position: absolute; 10 | } 11 | 12 | #header { 13 | height: 3em; 14 | width: 100%; 15 | display: flex; 16 | flex-flow: row nowrap; 17 | justify-content: space-between; 18 | align-items: center; 19 | } 20 | 21 | #sidebar { 22 | width: 25%; 23 | left: 0; 24 | top: 3em; 25 | bottom: 0; 26 | overflow-y: scroll; 27 | } 28 | 29 | #preview { 30 | width: 75%; 31 | right: 0; 32 | top: 3em; 33 | bottom: 0; 34 | } 35 | 36 | #header > * { 37 | height: 2em; 38 | margin: 0 0.5em; 39 | } 40 | 41 | #login { 42 | font-family: ArtifaktElement; 43 | font-size: 1em; 44 | } 45 | 46 | #header .title { 47 | height: auto; 48 | margin-right: auto; 49 | } 50 | 51 | #tree { 52 | margin: 0.5em; 53 | } 54 | 55 | @media (max-width: 768px) { 56 | #sidebar { 57 | width: 100%; 58 | top: 3em; 59 | bottom: 75%; 60 | } 61 | 62 | #preview { 63 | width: 100%; 64 | top: 25%; 65 | bottom: 0; 66 | } 67 | } 68 | 69 | #aps-logo{ 70 | height: 1em; 71 | } 72 | 73 | .icon-hub:before { 74 | background-image: url(https://raw.githubusercontent.com/primer/octicons/main/icons/apps-16.svg); /* or https://raw.githubusercontent.com/primer/octicons/main/icons/stack-16.svg */ 75 | background-size: cover; 76 | } 77 | 78 | .icon-project:before { 79 | background-image: url(https://raw.githubusercontent.com/primer/octicons/main/icons/project-16.svg); /* or https://raw.githubusercontent.com/primer/octicons/main/icons/organization-16.svg */ 80 | background-size: cover; 81 | } 82 | 83 | .icon-my-folder:before { 84 | background-image: url(https://raw.githubusercontent.com/primer/octicons/main/icons/file-directory-16.svg); 85 | background-size: cover; 86 | } 87 | 88 | .icon-item:before { 89 | background-image: url(https://raw.githubusercontent.com/primer/octicons/main/icons/file-16.svg); 90 | background-size: cover; 91 | } 92 | 93 | .icon-version:before { 94 | background-image: url(https://raw.githubusercontent.com/primer/octicons/main/icons/clock-16.svg); 95 | background-size: cover; 96 | } 97 | 98 | #alert_boxes { 99 | margin-left: 45vw; 100 | } 101 | 102 | /* Messages for DB */ 103 | .alert-box { 104 | padding: 15px; 105 | border: 1px solid transparent; 106 | border-radius: 4px; 107 | z-index: 99; 108 | position: relative; 109 | flex: 1; 110 | } 111 | 112 | .updated { 113 | color: #3c763d; 114 | background-color: #dff0d8; 115 | border-color: #d6e9c6; 116 | position: relative; 117 | width: 10vw; 118 | font-weight: bold; 119 | } 120 | 121 | .ready { 122 | color: #3c763d; 123 | background-color: #dff0d8; 124 | border-color: #d6e9c6; 125 | display: none; 126 | width: 10vw; 127 | } 128 | 129 | .failure { 130 | color: #a94442; 131 | background-color: #f2dede; 132 | border-color: #ebccd1; 133 | display: none; 134 | width: 10vw; 135 | } 136 | 137 | .gathering { 138 | color: #8a6d3b; 139 | background-color: #fcf8e3; 140 | border-color: #faebcc; 141 | display: none; 142 | width: 10vw; 143 | } -------------------------------------------------------------------------------- /Controllers/AuthController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Autodesk.Authentication.Model; 4 | using Microsoft.AspNetCore.Http; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.Extensions.Logging; 7 | 8 | [ApiController] 9 | [Route("api/[controller]")] 10 | public class AuthController : ControllerBase 11 | { 12 | public record AccessToken(string access_token, long expires_in); 13 | 14 | private readonly ILogger _logger; 15 | private readonly APSService _APSService; 16 | 17 | public AuthController(ILogger logger, APSService APSService) 18 | { 19 | _logger = logger; 20 | _APSService = APSService; 21 | } 22 | 23 | public static async Task PrepareTokens(HttpRequest request, HttpResponse response, APSService APSService) 24 | { 25 | if (!request.Cookies.ContainsKey("internal_token")) 26 | { 27 | return null; 28 | } 29 | var tokens = new Tokens 30 | { 31 | PublicToken = request.Cookies["public_token"], 32 | InternalToken = request.Cookies["internal_token"], 33 | RefreshToken = request.Cookies["refresh_token"], 34 | ExpiresAt = DateTime.Parse(request.Cookies["expires_at"]) 35 | }; 36 | if (tokens.ExpiresAt < DateTime.Now.ToUniversalTime()) 37 | { 38 | tokens = await APSService.RefreshTokens(tokens); 39 | response.Cookies.Append("public_token", tokens.PublicToken); 40 | response.Cookies.Append("internal_token", tokens.InternalToken); 41 | response.Cookies.Append("refresh_token", tokens.RefreshToken); 42 | response.Cookies.Append("expires_at", tokens.ExpiresAt.ToString()); 43 | } 44 | return tokens; 45 | } 46 | 47 | [HttpGet("login")] 48 | public ActionResult Login() 49 | { 50 | var redirectUri = _APSService.GetAuthorizationURL(); 51 | return Redirect(redirectUri); 52 | } 53 | 54 | [HttpGet("logout")] 55 | public ActionResult Logout() 56 | { 57 | Response.Cookies.Delete("public_token"); 58 | Response.Cookies.Delete("internal_token"); 59 | Response.Cookies.Delete("refresh_token"); 60 | Response.Cookies.Delete("expires_at"); 61 | return Redirect("/"); 62 | } 63 | 64 | [HttpGet("callback")] 65 | public async Task Callback(string code) 66 | { 67 | var tokens = await _APSService.GenerateTokens(code); 68 | Response.Cookies.Append("public_token", tokens.PublicToken); 69 | Response.Cookies.Append("internal_token", tokens.InternalToken); 70 | Response.Cookies.Append("refresh_token", tokens.RefreshToken); 71 | Response.Cookies.Append("expires_at", tokens.ExpiresAt.ToString()); 72 | return Redirect("/"); 73 | } 74 | 75 | [HttpGet("profile")] 76 | public async Task GetProfile(string? code) 77 | { 78 | var tokens = await PrepareTokens(Request, Response, _APSService); 79 | if (tokens == null) 80 | { 81 | return Unauthorized(); 82 | } 83 | UserInfo profile = await _APSService.GetUserProfile(tokens); 84 | return new 85 | { 86 | name = profile.Name 87 | }; 88 | } 89 | 90 | [HttpGet("token")] 91 | public async Task GetPublicToken(string? code) 92 | { 93 | var token = await _APSService.GetPublicToken(); 94 | return new AccessToken( 95 | token.PublicToken, 96 | (long)Math.Round((token.ExpiresAt - DateTime.UtcNow).TotalSeconds) 97 | ); 98 | } 99 | } -------------------------------------------------------------------------------- /wwwroot/sidebar.js: -------------------------------------------------------------------------------- 1 | async function getJSON(url) { 2 | const resp = await fetch(url); 3 | if (!resp.ok) { 4 | alert('Could not load tree data. See console for more details.'); 5 | console.error(await resp.text()); 6 | return []; 7 | } 8 | return resp.json(); 9 | } 10 | 11 | function createTreeNode(id, text, icon, children = false) { 12 | return { id, text, children, itree: { icon } }; 13 | } 14 | 15 | async function getHubs() { 16 | const hubs = await getJSON('/api/hubs'); 17 | return hubs.map(hub => createTreeNode(`hub|${hub.id}`, hub.attributes.name, 'icon-hub', true)); 18 | } 19 | 20 | async function getBucket() { 21 | const bucket = await getJSON('/api/models/bucket'); 22 | return [createTreeNode(`hub|${bucket.name}`, bucket.name, 'icon-hub', true)]; 23 | } 24 | 25 | async function getProjects(hubId) { 26 | const projects = await getJSON(`/api/hubs/${hubId}/projects`); 27 | return projects.map(project => createTreeNode(`project|${hubId}|${project.id}`, project.attributes.name, 'icon-project', true)); 28 | } 29 | 30 | async function getContents(hubId, projectId, folderId = null) { 31 | const contents = await getJSON(`/api/hubs/${hubId}/projects/${projectId}/contents` + (folderId ? `?folder_id=${folderId}` : '')); 32 | return contents.map(item => { 33 | if (item.type === 'folders') { 34 | return createTreeNode(`folder|${hubId}|${projectId}|${item.id}`, item.name, 'icon-my-folder', true); 35 | } else { 36 | return createTreeNode(`item|${hubId}|${projectId}|${item.id}`, item.name, 'icon-item', true); 37 | } 38 | }); 39 | } 40 | 41 | async function getVersions(hubId, projectId, itemId) { 42 | const versions = await getJSON(`/api/hubs/${hubId}/projects/${projectId}/contents/${itemId}/versions`); 43 | return versions.map(version => createTreeNode(`version|${version.id}`, version.createTime, 'icon-version')); 44 | } 45 | 46 | async function getObjects() { 47 | const objects = await getJSON(`/api/models`); 48 | return objects.map(object => createTreeNode(`object|${object.urn}`, object.name, 'icon-version')); 49 | } 50 | 51 | export function initOssTree(selector, onSelectionChanged) { 52 | // See http://inspire-tree.com 53 | const tree = new InspireTree({ 54 | data: function (node) { 55 | if (!node || !node.id) { 56 | return getBucket(); 57 | } else { 58 | const tokens = node.id.split('|'); 59 | return getObjects(tokens[1]); 60 | } 61 | } 62 | }); 63 | tree.on('node.click', function (event, node) { 64 | event.preventTreeDefault(); 65 | const tokens = node.id.split('|'); 66 | if (tokens[0] === 'object') { 67 | onSelectionChanged(tokens[1]); 68 | } 69 | }); 70 | return new InspireTreeDOM(tree, { target: selector }); 71 | } 72 | 73 | export function initHubsTree(selector, onSelectionChanged) { 74 | // See http://inspire-tree.com 75 | const tree = new InspireTree({ 76 | data: function (node) { 77 | if (!node || !node.id) { 78 | return getHubs(); 79 | } else { 80 | const tokens = node.id.split('|'); 81 | switch (tokens[0]) { 82 | case 'hub': return getProjects(tokens[1]); 83 | case 'project': return getContents(tokens[1], tokens[2]); 84 | case 'folder': return getContents(tokens[1], tokens[2], tokens[3]); 85 | case 'item': return getVersions(tokens[1], tokens[2], tokens[3]); 86 | default: return []; 87 | } 88 | } 89 | } 90 | }); 91 | tree.on('node.click', function (event, node) { 92 | event.preventTreeDefault(); 93 | const tokens = node.id.split('|'); 94 | if (tokens[0] === 'version') { 95 | onSelectionChanged(tokens[1]); 96 | } 97 | }); 98 | return new InspireTreeDOM(tree, { target: selector }); 99 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # aps-db-sample 2 | 3 | ![.NET](https://img.shields.io/badge/.NET-6-blue.svg) 4 | [![MIT](https://img.shields.io/badge/License-MIT-blue.svg)](http://opensource.org/licenses/MIT) 5 | [![Level](https://img.shields.io/badge/Level-Intermediate-blue.svg)](http://developer.autodesk.com/) 6 | 7 | [![oAuth2](https://img.shields.io/badge/oAuth2-v1-green.svg)](http://developer.autodesk.com/) 8 | [![Data-Management](https://img.shields.io/badge/Data%20Management-v2-green.svg)](http://developer.autodesk.com/) 9 | [![BIM360](https://img.shields.io/badge/BIM360-v1-green.svg)](http://developer.autodesk.com/) 10 | [![ACC](https://img.shields.io/badge/ACC-v1-green.svg)](http://developer.autodesk.com/) 11 | 12 | ## Thumbnail 13 | 14 | ![Thumbnail](thumbnail.png) 15 | 16 | ## Description 17 | 18 | This sample demonstrates how to connect with an external DB on demand, adding custom properties to Viewer property panel. 19 | It uses Websocket to notify different users when the parameters they might be seeing changes. 20 | We can understand how it works dividing the connection to DB in two sections. 21 | 22 | First, Reading the Data from DB and aggregating into property Panel. 23 | 24 | ![](README/READFROMDB.png) 25 | 26 | 1 - User select an element from the scene, which triggers a job to retrieve associated properties (specific to this element) from an external DB (MongoDB in this case) 27 | 28 | 2 - A task retrieves the associated properties from the external DB. 29 | 30 | 3 - If those properties exists they are returned and aggregated to the properties panel inside an input element, so the user can change them. If not, the properties are aggregated on properties panel as blank inputs, so user can write values to those, creating new values in the external DB. 31 | 32 | Then, updating the properties from Viewer. 33 | 34 | ![](README/UPDATEDBDATA.png) 35 | 36 | 1 - User update a custom parameter on viewer’s property panel inputs, which triggers a job to update this parameters value in the external DB (MongoDB in this case). 37 | 38 | 2 - A task updates the property value (or create it, if it doesn’t exists). 39 | 40 | 3 - The client that triggered the task (A) get notified about the result, while other clients (B and C) receives a notification with the changed/created value. 41 | 42 | # Setup 43 | 44 | ## Prerequisites 45 | 46 | 1. **APS Account**: Learn how to create a APS Account, activate subscription and create an app at [this tutorial](http://learnforge.autodesk.io/#/account/). 47 | 2. **Visual Studio**: Either Community (Windows) or Code (Windows, MacOS). 48 | 3. **.NET 6**: basic knowledge of C#. 49 | 4. **MongoDB Atlas**: Cloud-hosted MongoDB [refer here](https://www.mongodb.com/cloud/atlas/). 50 | 51 | Use of this sample requires Autodesk developer credentials. 52 | Visit the [APS Developer Portal](https://developer.autodesk.com), sign up for an account 53 | and [create an app](https://developer.autodesk.com/myapps/create) that uses Data Management and Model Derivative APIs. 54 | For this new app, use `http://localhost:3000/api/aps/callback/oauth` as Callback URL, although is not used in a 2-legged flow. 55 | Finally, make a note of the **Client ID** and **Client Secret**. 56 | 57 | ## Running locally 58 | 59 | Clone this project or download it. 60 | We recommend installing [GitHub desktop](https://desktop.github.com/). 61 | To clone it via command line, use the following (**Terminal** on MacOSX/Linux, **Git Shell** on Windows): 62 | 63 | git clone https://github.com/autodesk-platform-services/aps-db-sample 64 | 65 | **Environment variables** 66 | 67 | At the `.appsettings.Development.json`, find the env vars and add your APS Client ID, Secret and callback URL. The end result should be as shown below: 68 | 69 | ```json 70 | "APS_CLIENT_ID": "your APS app client id", 71 | "APS_CLIENT_SECRET": "your APS app client secret", 72 | "APS_CALLBACK_URL": "http://localhost:8080/api/auth/callback", 73 | "APS_BUCKET": "your bucket name", 74 | "MONGODB_CON_STRING": "your MongoDB connection string", 75 | "MONGODB_DBNAME": "your MongoDB DB name", 76 | "MONGODB_COLLECTION": "your MongoDB collection name", 77 | "DB_PROPERTIES_NAMES": "your comma separated properties" 78 | ``` 79 | 80 | ## License 81 | 82 | This sample is licensed under the terms of the [MIT License](http://opensource.org/licenses/MIT). Please see the [LICENSE](LICENSE) file for full details. 83 | 84 | ## Written by 85 | 86 | Joao Martins [@JooPaulodeOrne2](https://twitter.com/JooPaulodeOrne2), [Developer Advocate](http://aps.autodesk.com) 87 | -------------------------------------------------------------------------------- /Models/DBService.MongoDB.cs: -------------------------------------------------------------------------------- 1 | 2 | using aps_viewer_db_properties.Hubs; 3 | using Microsoft.AspNetCore.SignalR; 4 | using MongoDB.Bson; 5 | using MongoDB.Bson.Serialization; 6 | using MongoDB.Driver; 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Threading.Tasks; 10 | 11 | public partial class DBService 12 | { 13 | public async Task UpdateDataFromMongoDB(string connectionId, Property property, string selecteddbId, string itemId, IHubContext _dbHub) 14 | { 15 | 16 | var client = new MongoClient(_mongodbdatabaseConnectionString); 17 | 18 | var database = client.GetDatabase(_mongodbdatabaseName); 19 | 20 | var collection = database.GetCollection(_mongodbcollection); 21 | 22 | string id = GetIdFromProps(itemId, selecteddbId); 23 | 24 | var filter = new BsonDocument { { "_id", id } }; 25 | 26 | var updateDef = Builders.Update.Set(doc => doc[property.name], property.value); 27 | 28 | UpdateResult updateResult = collection.UpdateOne(filter, updateDef); 29 | 30 | bool createResult = false; 31 | 32 | if (updateResult.MatchedCount == 0) 33 | { 34 | createResult = await CreateNewItemFromMongo(collection, id, property); 35 | } 36 | 37 | string message = (updateResult.IsModifiedCountAvailable ? $"{updateResult.ModifiedCount} item modified!" : createResult ? "New Document created!" : "Error! No Document created/modified!"); 38 | 39 | Dictionary newRow = new Dictionary(); 40 | newRow[property.name] = property.value; 41 | 42 | DBHub.SendUpdate(_dbHub, connectionId, selecteddbId, updateResult.IsModifiedCountAvailable, message, newRow, itemId); 43 | 44 | } 45 | 46 | public async Task ExtractDataFromMongoDB(string connectionId, string selecteddbId, string itemId, IHubContext _dbHub) 47 | { 48 | 49 | var client = new MongoClient(_mongodbdatabaseConnectionString); 50 | 51 | var database = client.GetDatabase(_mongodbdatabaseName); 52 | 53 | var collection = database.GetCollection(_mongodbcollection); 54 | 55 | string id = GetIdFromProps(itemId, selecteddbId); 56 | 57 | var filter = new BsonDocument { { "_id", id } }; 58 | 59 | Dictionary newRow = new Dictionary(); 60 | 61 | try 62 | { 63 | BsonDocument bsonDocument = await collection.Find(filter).SingleAsync(); 64 | Dictionary document = bsonDocument.ToDictionary(); 65 | 66 | foreach (string field in _properties.Split(",")) 67 | { 68 | try 69 | { 70 | newRow[field] = document[field]; 71 | } 72 | catch (Exception keyEx) 73 | { 74 | //In this case we have a field on env variable that's not present on Mongo Document 75 | newRow[field] = ""; 76 | 77 | } 78 | } 79 | } 80 | catch (Exception ex) 81 | { 82 | //In this case no the document related to this element doesn't exists 83 | foreach (string field in _properties.Split(",")) 84 | { 85 | newRow[field] = ""; 86 | } 87 | } 88 | await DBHub.SendData(_dbHub, connectionId, selecteddbId, newRow); 89 | } 90 | 91 | public async Task CreateNewItemFromMongo(dynamic collection, string id, Property property) 92 | { 93 | bool response = true; 94 | 95 | Dictionary newDocument = new Dictionary(); 96 | newDocument["_id"] = id; 97 | newDocument[property.name] = property.value; 98 | 99 | //foreach (string field in propFields.Split(",")) 100 | //{ 101 | // try 102 | // { 103 | // newDocument[field] = field == property.name ? property.value : ""; 104 | // } 105 | // catch (Exception ex) 106 | // { 107 | // 108 | // } 109 | //} 110 | 111 | try 112 | { 113 | var jsonDoc = Newtonsoft.Json.JsonConvert.SerializeObject(newDocument); 114 | var bsonDoc = BsonSerializer.Deserialize(jsonDoc); 115 | collection.InsertOne(bsonDoc); 116 | } 117 | catch (Exception ex) 118 | { 119 | response = false; 120 | } 121 | 122 | return response; 123 | 124 | } 125 | 126 | //Through this function we obtain the id used by Mongo based on our model 127 | public string GetIdFromProps(string itemId, string selecteddbId) 128 | { 129 | return $"{itemId}_{selecteddbId}"; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Visual Studio code coverage results 141 | *.coverage 142 | *.coveragexml 143 | 144 | # NCrunch 145 | _NCrunch_* 146 | .*crunch*.local.xml 147 | nCrunchTemp_* 148 | 149 | # MightyMoose 150 | *.mm.* 151 | AutoTest.Net/ 152 | 153 | # Web workbench (sass) 154 | .sass-cache/ 155 | 156 | # Installshield output folder 157 | [Ee]xpress/ 158 | 159 | # DocProject is a documentation generator add-in 160 | DocProject/buildhelp/ 161 | DocProject/Help/*.HxT 162 | DocProject/Help/*.HxC 163 | DocProject/Help/*.hhc 164 | DocProject/Help/*.hhk 165 | DocProject/Help/*.hhp 166 | DocProject/Help/Html2 167 | DocProject/Help/html 168 | 169 | # Click-Once directory 170 | publish/ 171 | 172 | # Publish Web Output 173 | *.[Pp]ublish.xml 174 | *.azurePubxml 175 | # Note: Comment the next line if you want to checkin your web deploy settings, 176 | # but database connection strings (with potential passwords) will be unencrypted 177 | *.pubxml 178 | *.publishproj 179 | 180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 181 | # checkin your Azure Web App publish settings, but sensitive information contained 182 | # in these scripts will be unencrypted 183 | PublishScripts/ 184 | 185 | # NuGet Packages 186 | *.nupkg 187 | # NuGet Symbol Packages 188 | *.snupkg 189 | # The packages folder can be ignored because of Package Restore 190 | **/[Pp]ackages/* 191 | # except build/, which is used as an MSBuild target. 192 | !**/[Pp]ackages/build/ 193 | # Uncomment if necessary however generally it will be regenerated when needed 194 | #!**/[Pp]ackages/repositories.config 195 | # NuGet v3's project.json files produces more ignorable files 196 | *.nuget.props 197 | *.nuget.targets 198 | 199 | # Microsoft Azure Build Output 200 | csx/ 201 | *.build.csdef 202 | 203 | # Microsoft Azure Emulator 204 | ecf/ 205 | rcf/ 206 | 207 | # Windows Store app package directories and files 208 | AppPackages/ 209 | BundleArtifacts/ 210 | Package.StoreAssociation.xml 211 | _pkginfo.txt 212 | *.appx 213 | *.appxbundle 214 | *.appxupload 215 | 216 | # Visual Studio cache files 217 | # files ending in .cache can be ignored 218 | *.[Cc]ache 219 | # but keep track of directories ending in .cache 220 | !?*.[Cc]ache/ 221 | 222 | # Others 223 | ClientBin/ 224 | ~$* 225 | *~ 226 | *.dbmdl 227 | *.dbproj.schemaview 228 | *.jfm 229 | *.pfx 230 | *.publishsettings 231 | orleans.codegen.cs 232 | 233 | # Including strong name files can present a security risk 234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 235 | #*.snk 236 | 237 | # Since there are multiple workflows, uncomment next line to ignore bower_components 238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 239 | #bower_components/ 240 | 241 | # RIA/Silverlight projects 242 | Generated_Code/ 243 | 244 | # Backup & report files from converting an old project file 245 | # to a newer Visual Studio version. Backup files are not needed, 246 | # because we have git ;-) 247 | _UpgradeReport_Files/ 248 | Backup*/ 249 | UpgradeLog*.XML 250 | UpgradeLog*.htm 251 | ServiceFabricBackup/ 252 | *.rptproj.bak 253 | 254 | # SQL Server files 255 | *.mdf 256 | *.ldf 257 | *.ndf 258 | 259 | # Business Intelligence projects 260 | *.rdl.data 261 | *.bim.layout 262 | *.bim_*.settings 263 | *.rptproj.rsuser 264 | *- [Bb]ackup.rdl 265 | *- [Bb]ackup ([0-9]).rdl 266 | *- [Bb]ackup ([0-9][0-9]).rdl 267 | 268 | # Microsoft Fakes 269 | FakesAssemblies/ 270 | 271 | # GhostDoc plugin setting file 272 | *.GhostDoc.xml 273 | 274 | # Node.js Tools for Visual Studio 275 | .ntvs_analysis.dat 276 | node_modules/ 277 | 278 | # Visual Studio 6 build log 279 | *.plg 280 | 281 | # Visual Studio 6 workspace options file 282 | *.opt 283 | 284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 285 | *.vbw 286 | 287 | # Visual Studio LightSwitch build output 288 | **/*.HTMLClient/GeneratedArtifacts 289 | **/*.DesktopClient/GeneratedArtifacts 290 | **/*.DesktopClient/ModelManifest.xml 291 | **/*.Server/GeneratedArtifacts 292 | **/*.Server/ModelManifest.xml 293 | _Pvt_Extensions 294 | 295 | # Paket dependency manager 296 | .paket/paket.exe 297 | paket-files/ 298 | 299 | # FAKE - F# Make 300 | .fake/ 301 | 302 | # CodeRush personal settings 303 | .cr/personal 304 | 305 | # Python Tools for Visual Studio (PTVS) 306 | __pycache__/ 307 | *.pyc 308 | 309 | # Cake - Uncomment if you are using it 310 | # tools/** 311 | # !tools/packages.config 312 | 313 | # Tabs Studio 314 | *.tss 315 | 316 | # Telerik's JustMock configuration file 317 | *.jmconfig 318 | 319 | # BizTalk build output 320 | *.btp.cs 321 | *.btm.cs 322 | *.odx.cs 323 | *.xsd.cs 324 | 325 | # OpenCover UI analysis results 326 | OpenCover/ 327 | 328 | # Azure Stream Analytics local run output 329 | ASALocalRun/ 330 | 331 | # MSBuild Binary and Structured Log 332 | *.binlog 333 | 334 | # NVidia Nsight GPU debugger configuration file 335 | *.nvuser 336 | 337 | # MFractors (Xamarin productivity tool) working folder 338 | .mfractor/ 339 | 340 | # Local History for Visual Studio 341 | .localhistory/ 342 | 343 | # BeatPulse healthcheck temp database 344 | healthchecksdb 345 | 346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 347 | MigrationBackup/ 348 | 349 | # Ionide (cross platform F# VS Code tools) working folder 350 | .ionide/ 351 | -------------------------------------------------------------------------------- /wwwroot/Extensions/DBPropertiesExtension.js: -------------------------------------------------------------------------------- 1 | ///////////////////////////////////////////////////////////////////// 2 | // Copyright (c) Autodesk, Inc. All rights reserved 3 | // Written by APS Partner Development 4 | // 5 | // Permission to use, copy, modify, and distribute this software in 6 | // object code form for any purpose and without fee is hereby granted, 7 | // provided that the above copyright notice appears in all copies and 8 | // that both that copyright notice and the limited warranty and 9 | // restricted rights notice below appear in all supporting 10 | // documentation. 11 | // 12 | // AUTODESK PROVIDES THIS PROGRAM "AS IS" AND WITH ALL FAULTS. 13 | // AUTODESK SPECIFICALLY DISCLAIMS ANY IMPLIED WARRANTY OF 14 | // MERCHANTABILITY OR FITNESS FOR A PARTICULAR USE. AUTODESK, INC. 15 | // DOES NOT WARRANT THAT THE OPERATION OF THE PROGRAM WILL BE 16 | // UNINTERRUPTED OR ERROR FREE. 17 | ///////////////////////////////////////////////////////////////////// 18 | 19 | // ******************************************* 20 | // DB Property Panel 21 | // ******************************************* 22 | class DBPropertyPanel extends Autodesk.Viewing.Extensions.ViewerPropertyPanel { 23 | constructor(viewer, options) { 24 | super(viewer, options); 25 | this.properties = options.properties || {}; 26 | this.currentProperty = null; 27 | this.dbId = ""; 28 | this.modelUrn = _viewer.model.getSeedUrn(); 29 | 30 | //This is the event for property doubleclick 31 | //Autodesk.Viewing.UI.PropertyPanel.prototype.onPropertyDoubleClick = this.handlePropertyUpdate; 32 | //This in the event for object selection changes 33 | viewer.addEventListener(Autodesk.Viewing.SELECTION_CHANGED_EVENT, this.queryProps.bind(this)); 34 | } 35 | 36 | queryProps(method) { 37 | if (_viewer.getSelection().length == 1) { 38 | this.dbId = _viewer.getSelection()[0]; 39 | method === 'update' ? this.updateDB(this.dbId) : this.queryDB(this.dbId); 40 | } 41 | } 42 | 43 | updateDB(selecteddbId) { 44 | let itemId = this.modelUrn; 45 | updateDBData(selecteddbId, this.currentProperty, itemId); 46 | } 47 | 48 | queryDB(selecteddbId) { 49 | let itemId = this.modelUrn; 50 | extractDBData(selecteddbId, itemId); 51 | } 52 | 53 | setAggregatedProperties(propertySet) { 54 | Autodesk.Viewing.Extensions.ViewerPropertyPanel.prototype.setAggregatedProperties.call(this, propertySet); 55 | 56 | // add your custom properties here 57 | const dbids = propertySet.getDbIds(); 58 | dbids.forEach(id => { 59 | this.setdbIdProperties(id); 60 | }); 61 | } 62 | 63 | setdbIdProperties(dbId) { 64 | var propsForObject = this.properties[dbId.toString()]; 65 | if (propsForObject) { 66 | let _document = document; 67 | for (const groupName in propsForObject) { 68 | const group = propsForObject[groupName]; 69 | for (const propName in group) { 70 | const propValue = group[propName]; 71 | if (!this.tree.getElementForNode({ name: propName, value: "", category: CustomCategoryName })) { 72 | this.addProperty(propName, "", CustomCategoryName); 73 | } 74 | let element = this.tree.getElementForNode({ name: propName, value: "", category: CustomCategoryName }); 75 | let inputValue = _document.createElement("input"); 76 | inputValue.type = "text"; 77 | inputValue.placeholder = propValue; 78 | inputValue.value = propValue; 79 | inputValue.addEventListener("focusout", () => { 80 | this.handlePropertyUpdate.call(this, inputValue, propName); 81 | }); 82 | element.children[0].children[3].innerHTML = ''; 83 | element.children[0].children[3].appendChild(inputValue); 84 | } 85 | } 86 | //this.highlight(CustomCategoryName); 87 | } 88 | } 89 | 90 | updateCurrentProperty(newValue) { 91 | this.properties[this.dbId][this.currentProperty.category][this.currentProperty.name] = newValue; 92 | this.currentProperty.value = newValue; 93 | } 94 | 95 | handlePropertyUpdate(input, propName) { 96 | let newPropValue = input.value; 97 | let propValue = this.properties[this.dbId][CustomCategoryName][propName]; 98 | this.currentProperty = { 99 | name: propName, 100 | value: propValue, 101 | category: CustomCategoryName 102 | }; 103 | if (propValue !== newPropValue) { 104 | this.updateCurrentProperty(newPropValue); 105 | this.queryProps('update'); 106 | console.log("Current property changed to " + propName + " : " + newPropValue); 107 | } 108 | } 109 | 110 | checkProperty(property) { 111 | try { 112 | //Here we check if the property selected is aquired from DB 113 | return this.properties[this.dbId][property.category][property.name] == property.value; 114 | } 115 | catch { 116 | return false; 117 | } 118 | } 119 | }; 120 | 121 | var CustomCategoryName = "Properties From DB"; 122 | 123 | //Here we show the user the result about the updated parameter 124 | async function showUpdateResult(selecteddbId, updateResult) { 125 | let selector = (updateResult ? "div.ready" : "div.failure") 126 | $(selector).fadeIn(500).delay(2000).fadeOut(500); 127 | } 128 | 129 | async function showNotification(selecteddbId) { 130 | $('#alert_boxes').append( 131 | `
${selecteddbId + ' updated!'}
` 132 | ); 133 | setTimeout(() => { 134 | $('#alert_boxes').find(':last-child').fadeIn(500).delay(8000).fadeOut(500, function () { $(this).remove(); }); 135 | }, 100); 136 | } 137 | 138 | async function highlightDbId(event, selecteddbId) { 139 | event.target.remove(); 140 | _viewer.isolate(selecteddbId); 141 | _viewer.fitToView(selecteddbId); 142 | } 143 | 144 | //Here we add the properties aquired from DB to the proper dbid proper property panel 145 | async function addProperties(selecteddbId, properties) { 146 | let ext = _viewer.getExtension('DBPropertiesExtension'); 147 | 148 | ext.panel.properties[selecteddbId] = { 149 | [CustomCategoryName]: { 150 | 151 | } 152 | }; 153 | for (const property of Object.keys(properties)) { 154 | ext.panel.properties[selecteddbId][[CustomCategoryName]][property] = properties[property]; 155 | } 156 | ext.panel.setdbIdProperties(selecteddbId); 157 | //ext.panel.highlightCustomProperties(); 158 | } 159 | 160 | //Here we reach the server endpoint to update the proper data from DB 161 | async function updateDBData(selecteddbId, property, itemId) { 162 | const requestUrl = '/api/db/dbconnector'; 163 | const requestData = { 164 | 'connectionId': connection.connection.connectionId, 165 | 'dbProvider': 'mongo', 166 | 'property': property, 167 | 'selecteddbId': selecteddbId, 168 | 'itemId': itemId 169 | }; 170 | apiClientAsync(requestUrl, requestData, 'post'); 171 | $("div.gathering").fadeIn(500).delay(1500).fadeOut(500); 172 | } 173 | 174 | //Here we reach the server endpoint to retrieve the proper data from DB 175 | async function extractDBData(selecteddbId, itemId) { 176 | try { 177 | const requestUrl = '/api/db/dbconnector'; 178 | const requestData = { 179 | 'connectionId': connection.connection.connectionId, 180 | 'selecteddbId': selecteddbId, 181 | 'itemId': itemId, 182 | 'dbProvider': 'mongo' 183 | }; 184 | apiClientAsync(requestUrl, requestData); 185 | $("div.gathering").fadeIn(500).delay(1500).fadeOut(500); 186 | } 187 | catch (err) { 188 | console.log(err); 189 | $("div.failure").fadeIn(500).delay(1500).fadeOut(500); 190 | } 191 | } 192 | 193 | // helper function for Request 194 | function apiClientAsync(requestUrl, requestData = null, requestMethod = 'get') { 195 | let def = $.Deferred(); 196 | 197 | if (requestMethod == 'post') { 198 | requestData = JSON.stringify(requestData); 199 | } 200 | 201 | jQuery.ajax({ 202 | url: requestUrl, 203 | contentType: 'application/json', 204 | type: requestMethod, 205 | dataType: 'json', 206 | data: requestData, 207 | success: function (res) { 208 | def.resolve(res); 209 | }, 210 | error: function (err) { 211 | console.error('request failed:'); 212 | def.reject(err) 213 | } 214 | }); 215 | return def.promise(); 216 | } 217 | 218 | var _viewer; 219 | 220 | // ******************************************* 221 | // DB Properties Extension 222 | // ******************************************* 223 | class DBPropertiesExtension extends Autodesk.Viewing.Extension { 224 | constructor(viewer, options) { 225 | super(viewer, options); 226 | _viewer = viewer; 227 | 228 | this.panel = new DBPropertyPanel(viewer, options); 229 | } 230 | 231 | async load() { 232 | var ext = await this.viewer.getExtension('Autodesk.PropertiesManager'); 233 | ext.setPanel(this.panel); 234 | 235 | return true; 236 | } 237 | 238 | async unload() { 239 | var ext = await this.viewer.getExtension('Autodesk.PropertiesManager'); 240 | ext.setDefaultPanel(); 241 | 242 | return true; 243 | } 244 | } 245 | 246 | Autodesk.Viewing.theExtensionManager.registerExtension('DBPropertiesExtension', DBPropertiesExtension); -------------------------------------------------------------------------------- /wwwroot/signalr/dist/browser/signalr.min.js: -------------------------------------------------------------------------------- 1 | var t,e;t=self,e=function(){return(()=>{var t={d:(e,s)=>{for(var n in s)t.o(s,n)&&!t.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:s[n]})}};t.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(t){if("object"==typeof window)return window}}(),t.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e),t.r=t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"t",{value:!0})};var e,s={};t.r(s),t.d(s,{AbortError:()=>r,DefaultHttpClient:()=>H,HttpClient:()=>d,HttpError:()=>n,HttpResponse:()=>u,HttpTransportType:()=>M,HubConnection:()=>N,HubConnectionBuilder:()=>Q,HubConnectionState:()=>R,JsonHubProtocol:()=>G,LogLevel:()=>e,MessageType:()=>A,NullLogger:()=>f,Subject:()=>U,TimeoutError:()=>i,TransferFormat:()=>W,VERSION:()=>p});class n extends Error{constructor(t,e){const s=new.target.prototype;super(`${t}: Status code '${e}'`),this.statusCode=e,this.__proto__=s}}class i extends Error{constructor(t="A timeout occurred."){const e=new.target.prototype;super(t),this.__proto__=e}}class r extends Error{constructor(t="An abort occurred."){const e=new.target.prototype;super(t),this.__proto__=e}}class o extends Error{constructor(t,e){const s=new.target.prototype;super(t),this.transport=e,this.errorType="UnsupportedTransportError",this.__proto__=s}}class h extends Error{constructor(t,e){const s=new.target.prototype;super(t),this.transport=e,this.errorType="DisabledTransportError",this.__proto__=s}}class c extends Error{constructor(t,e){const s=new.target.prototype;super(t),this.transport=e,this.errorType="FailedToStartTransportError",this.__proto__=s}}class a extends Error{constructor(t){const e=new.target.prototype;super(t),this.errorType="FailedToNegotiateWithServerError",this.__proto__=e}}class l extends Error{constructor(t,e){const s=new.target.prototype;super(t),this.innerErrors=e,this.__proto__=s}}class u{constructor(t,e,s){this.statusCode=t,this.statusText=e,this.content=s}}class d{get(t,e){return this.send({...e,method:"GET",url:t})}post(t,e){return this.send({...e,method:"POST",url:t})}delete(t,e){return this.send({...e,method:"DELETE",url:t})}getCookieString(t){return""}}!function(t){t[t.Trace=0]="Trace",t[t.Debug=1]="Debug",t[t.Information=2]="Information",t[t.Warning=3]="Warning",t[t.Error=4]="Error",t[t.Critical=5]="Critical",t[t.None=6]="None"}(e||(e={}));class f{constructor(){}log(t,e){}}f.instance=new f;const p="6.0.6";class w{static isRequired(t,e){if(null==t)throw new Error(`The '${e}' argument is required.`)}static isNotEmpty(t,e){if(!t||t.match(/^\s*$/))throw new Error(`The '${e}' argument should not be empty.`)}static isIn(t,e,s){if(!(t in e))throw new Error(`Unknown ${s} value: ${t}.`)}}class g{static get isBrowser(){return"object"==typeof window&&"object"==typeof window.document}static get isWebWorker(){return"object"==typeof self&&"importScripts"in self}static get isReactNative(){return"object"==typeof window&&void 0===window.document}static get isNode(){return!this.isBrowser&&!this.isWebWorker&&!this.isReactNative}}function y(t,e){let s="";return m(t)?(s=`Binary data of length ${t.byteLength}`,e&&(s+=`. Content: '${function(t){const e=new Uint8Array(t);let s="";return e.forEach((t=>{s+=`0x${t<16?"0":""}${t.toString(16)} `})),s.substr(0,s.length-1)}(t)}'`)):"string"==typeof t&&(s=`String data of length ${t.length}`,e&&(s+=`. Content: '${t}'`)),s}function m(t){return t&&"undefined"!=typeof ArrayBuffer&&(t instanceof ArrayBuffer||t.constructor&&"ArrayBuffer"===t.constructor.name)}async function b(t,s,n,i,r,o,h){let c={};if(r){const t=await r();t&&(c={Authorization:`Bearer ${t}`})}const[a,l]=$();c[a]=l,t.log(e.Trace,`(${s} transport) sending data. ${y(o,h.logMessageContent)}.`);const u=m(o)?"arraybuffer":"text",d=await n.post(i,{content:o,headers:{...c,...h.headers},responseType:u,timeout:h.timeout,withCredentials:h.withCredentials});t.log(e.Trace,`(${s} transport) request complete. Response status: ${d.statusCode}.`)}class v{constructor(t,e){this.i=t,this.h=e}dispose(){const t=this.i.observers.indexOf(this.h);t>-1&&this.i.observers.splice(t,1),0===this.i.observers.length&&this.i.cancelCallback&&this.i.cancelCallback().catch((t=>{}))}}class E{constructor(t){this.l=t,this.out=console}log(t,s){if(t>=this.l){const n=`[${(new Date).toISOString()}] ${e[t]}: ${s}`;switch(t){case e.Critical:case e.Error:this.out.error(n);break;case e.Warning:this.out.warn(n);break;case e.Information:this.out.info(n);break;default:this.out.log(n)}}}}function $(){let t="X-SignalR-User-Agent";return g.isNode&&(t="User-Agent"),[t,C(p,S(),g.isNode?"NodeJS":"Browser",k())]}function C(t,e,s,n){let i="Microsoft SignalR/";const r=t.split(".");return i+=`${r[0]}.${r[1]}`,i+=` (${t}; `,i+=e&&""!==e?`${e}; `:"Unknown OS; ",i+=`${s}`,i+=n?`; ${n}`:"; Unknown Runtime Version",i+=")",i}function S(){if(!g.isNode)return"";switch(process.platform){case"win32":return"Windows NT";case"darwin":return"macOS";case"linux":return"Linux";default:return process.platform}}function k(){if(g.isNode)return process.versions.node}function P(t){return t.stack?t.stack:t.message?t.message:`${t}`}class T extends d{constructor(e){if(super(),this.u=e,"undefined"==typeof fetch){const t=require;this.p=new(t("tough-cookie").CookieJar),this.m=t("node-fetch"),this.m=t("fetch-cookie")(this.m,this.p)}else this.m=fetch.bind(function(){if("undefined"!=typeof globalThis)return globalThis;if("undefined"!=typeof self)return self;if("undefined"!=typeof window)return window;if(void 0!==t.g)return t.g;throw new Error("could not find global")}());if("undefined"==typeof AbortController){const t=require;this.v=t("abort-controller")}else this.v=AbortController}async send(t){if(t.abortSignal&&t.abortSignal.aborted)throw new r;if(!t.method)throw new Error("No method defined.");if(!t.url)throw new Error("No url defined.");const s=new this.v;let o;t.abortSignal&&(t.abortSignal.onabort=()=>{s.abort(),o=new r});let h,c=null;if(t.timeout){const n=t.timeout;c=setTimeout((()=>{s.abort(),this.u.log(e.Warning,"Timeout from HTTP request."),o=new i}),n)}try{h=await this.m(t.url,{body:t.content,cache:"no-cache",credentials:!0===t.withCredentials?"include":"same-origin",headers:{"Content-Type":"text/plain;charset=UTF-8","X-Requested-With":"XMLHttpRequest",...t.headers},method:t.method,mode:"cors",redirect:"follow",signal:s.signal})}catch(t){if(o)throw o;throw this.u.log(e.Warning,`Error from HTTP request. ${t}.`),t}finally{c&&clearTimeout(c),t.abortSignal&&(t.abortSignal.onabort=null)}if(!h.ok){const t=await I(h,"text");throw new n(t||h.statusText,h.status)}const a=I(h,t.responseType),l=await a;return new u(h.status,h.statusText,l)}getCookieString(t){let e="";return g.isNode&&this.p&&this.p.getCookies(t,((t,s)=>e=s.join("; "))),e}}function I(t,e){let s;switch(e){case"arraybuffer":s=t.arrayBuffer();break;case"text":s=t.text();break;case"blob":case"document":case"json":throw new Error(`${e} is not supported.`);default:s=t.text()}return s}class _ extends d{constructor(t){super(),this.u=t}send(t){return t.abortSignal&&t.abortSignal.aborted?Promise.reject(new r):t.method?t.url?new Promise(((s,o)=>{const h=new XMLHttpRequest;h.open(t.method,t.url,!0),h.withCredentials=void 0===t.withCredentials||t.withCredentials,h.setRequestHeader("X-Requested-With","XMLHttpRequest"),h.setRequestHeader("Content-Type","text/plain;charset=UTF-8");const c=t.headers;c&&Object.keys(c).forEach((t=>{h.setRequestHeader(t,c[t])})),t.responseType&&(h.responseType=t.responseType),t.abortSignal&&(t.abortSignal.onabort=()=>{h.abort(),o(new r)}),t.timeout&&(h.timeout=t.timeout),h.onload=()=>{t.abortSignal&&(t.abortSignal.onabort=null),h.status>=200&&h.status<300?s(new u(h.status,h.statusText,h.response||h.responseText)):o(new n(h.response||h.responseText||h.statusText,h.status))},h.onerror=()=>{this.u.log(e.Warning,`Error from HTTP request. ${h.status}: ${h.statusText}.`),o(new n(h.statusText,h.status))},h.ontimeout=()=>{this.u.log(e.Warning,"Timeout from HTTP request."),o(new i)},h.send(t.content||"")})):Promise.reject(new Error("No url defined.")):Promise.reject(new Error("No method defined."))}}class H extends d{constructor(t){if(super(),"undefined"!=typeof fetch||g.isNode)this.$=new T(t);else{if("undefined"==typeof XMLHttpRequest)throw new Error("No usable HttpClient found.");this.$=new _(t)}}send(t){return t.abortSignal&&t.abortSignal.aborted?Promise.reject(new r):t.method?t.url?this.$.send(t):Promise.reject(new Error("No url defined.")):Promise.reject(new Error("No method defined."))}getCookieString(t){return this.$.getCookieString(t)}}class D{static write(t){return`${t}${D.RecordSeparator}`}static parse(t){if(t[t.length-1]!==D.RecordSeparator)throw new Error("Message is incomplete.");const e=t.split(D.RecordSeparator);return e.pop(),e}}D.RecordSeparatorCode=30,D.RecordSeparator=String.fromCharCode(D.RecordSeparatorCode);class x{writeHandshakeRequest(t){return D.write(JSON.stringify(t))}parseHandshakeResponse(t){let e,s;if(m(t)){const n=new Uint8Array(t),i=n.indexOf(D.RecordSeparatorCode);if(-1===i)throw new Error("Message is incomplete.");const r=i+1;e=String.fromCharCode.apply(null,Array.prototype.slice.call(n.slice(0,r))),s=n.byteLength>r?n.slice(r).buffer:null}else{const n=t,i=n.indexOf(D.RecordSeparator);if(-1===i)throw new Error("Message is incomplete.");const r=i+1;e=n.substring(0,r),s=n.length>r?n.substring(r):null}const n=D.parse(e),i=JSON.parse(n[0]);if(i.type)throw new Error("Expected a handshake response from the server.");return[s,i]}}var A,R;!function(t){t[t.Invocation=1]="Invocation",t[t.StreamItem=2]="StreamItem",t[t.Completion=3]="Completion",t[t.StreamInvocation=4]="StreamInvocation",t[t.CancelInvocation=5]="CancelInvocation",t[t.Ping=6]="Ping",t[t.Close=7]="Close"}(A||(A={}));class U{constructor(){this.observers=[]}next(t){for(const e of this.observers)e.next(t)}error(t){for(const e of this.observers)e.error&&e.error(t)}complete(){for(const t of this.observers)t.complete&&t.complete()}subscribe(t){return this.observers.push(t),new v(this,t)}}!function(t){t.Disconnected="Disconnected",t.Connecting="Connecting",t.Connected="Connected",t.Disconnecting="Disconnecting",t.Reconnecting="Reconnecting"}(R||(R={}));class N{constructor(t,s,n,i){this.C=0,this.S=()=>{this.u.log(e.Warning,"The page is being frozen, this will likely lead to the connection being closed and messages being lost. For more information see the docs at https://docs.microsoft.com/aspnet/core/signalr/javascript-client#bsleep")},w.isRequired(t,"connection"),w.isRequired(s,"logger"),w.isRequired(n,"protocol"),this.serverTimeoutInMilliseconds=3e4,this.keepAliveIntervalInMilliseconds=15e3,this.u=s,this.k=n,this.connection=t,this.P=i,this.T=new x,this.connection.onreceive=t=>this.I(t),this.connection.onclose=t=>this._(t),this.H={},this.D={},this.A=[],this.R=[],this.U=[],this.N=0,this.L=!1,this.q=R.Disconnected,this.j=!1,this.M=this.k.writeMessage({type:A.Ping})}static create(t,e,s,n){return new N(t,e,s,n)}get state(){return this.q}get connectionId(){return this.connection&&this.connection.connectionId||null}get baseUrl(){return this.connection.baseUrl||""}set baseUrl(t){if(this.q!==R.Disconnected&&this.q!==R.Reconnecting)throw new Error("The HubConnection must be in the Disconnected or Reconnecting state to change the url.");if(!t)throw new Error("The HubConnection url must be a valid url.");this.connection.baseUrl=t}start(){return this.W=this.O(),this.W}async O(){if(this.q!==R.Disconnected)return Promise.reject(new Error("Cannot start a HubConnection that is not in the 'Disconnected' state."));this.q=R.Connecting,this.u.log(e.Debug,"Starting HubConnection.");try{await this.F(),g.isBrowser&&window.document.addEventListener("freeze",this.S),this.q=R.Connected,this.j=!0,this.u.log(e.Debug,"HubConnection connected successfully.")}catch(t){return this.q=R.Disconnected,this.u.log(e.Debug,`HubConnection failed to start successfully because of error '${t}'.`),Promise.reject(t)}}async F(){this.B=void 0,this.L=!1;const t=new Promise(((t,e)=>{this.X=t,this.J=e}));await this.connection.start(this.k.transferFormat);try{const s={protocol:this.k.name,version:this.k.version};if(this.u.log(e.Debug,"Sending handshake request."),await this.V(this.T.writeHandshakeRequest(s)),this.u.log(e.Information,`Using HubProtocol '${this.k.name}'.`),this.G(),this.K(),this.Y(),await t,this.B)throw this.B}catch(t){throw this.u.log(e.Debug,`Hub handshake failed with error '${t}' during start(). Stopping HubConnection.`),this.G(),this.Z(),await this.connection.stop(t),t}}async stop(){const t=this.W;this.tt=this.et(),await this.tt;try{await t}catch(t){}}et(t){return this.q===R.Disconnected?(this.u.log(e.Debug,`Call to HubConnection.stop(${t}) ignored because it is already in the disconnected state.`),Promise.resolve()):this.q===R.Disconnecting?(this.u.log(e.Debug,`Call to HttpConnection.stop(${t}) ignored because the connection is already in the disconnecting state.`),this.tt):(this.q=R.Disconnecting,this.u.log(e.Debug,"Stopping HubConnection."),this.st?(this.u.log(e.Debug,"Connection stopped during reconnect delay. Done reconnecting."),clearTimeout(this.st),this.st=void 0,this.nt(),Promise.resolve()):(this.G(),this.Z(),this.B=t||new Error("The connection was stopped before the hub handshake could complete."),this.connection.stop(t)))}stream(t,...e){const[s,n]=this.it(e),i=this.rt(t,e,n);let r;const o=new U;return o.cancelCallback=()=>{const t=this.ot(i.invocationId);return delete this.H[i.invocationId],r.then((()=>this.ht(t)))},this.H[i.invocationId]=(t,e)=>{e?o.error(e):t&&(t.type===A.Completion?t.error?o.error(new Error(t.error)):o.complete():o.next(t.item))},r=this.ht(i).catch((t=>{o.error(t),delete this.H[i.invocationId]})),this.ct(s,r),o}V(t){return this.Y(),this.connection.send(t)}ht(t){return this.V(this.k.writeMessage(t))}send(t,...e){const[s,n]=this.it(e),i=this.ht(this.at(t,e,!0,n));return this.ct(s,i),i}invoke(t,...e){const[s,n]=this.it(e),i=this.at(t,e,!1,n);return new Promise(((t,e)=>{this.H[i.invocationId]=(s,n)=>{n?e(n):s&&(s.type===A.Completion?s.error?e(new Error(s.error)):t(s.result):e(new Error(`Unexpected message type: ${s.type}`)))};const n=this.ht(i).catch((t=>{e(t),delete this.H[i.invocationId]}));this.ct(s,n)}))}on(t,e){t&&e&&(t=t.toLowerCase(),this.D[t]||(this.D[t]=[]),-1===this.D[t].indexOf(e)&&this.D[t].push(e))}off(t,e){if(!t)return;t=t.toLowerCase();const s=this.D[t];if(s)if(e){const n=s.indexOf(e);-1!==n&&(s.splice(n,1),0===s.length&&delete this.D[t])}else delete this.D[t]}onclose(t){t&&this.A.push(t)}onreconnecting(t){t&&this.R.push(t)}onreconnected(t){t&&this.U.push(t)}I(t){if(this.G(),this.L||(t=this.lt(t),this.L=!0),t){const s=this.k.parseMessages(t,this.u);for(const t of s)switch(t.type){case A.Invocation:this.ut(t);break;case A.StreamItem:case A.Completion:{const s=this.H[t.invocationId];if(s){t.type===A.Completion&&delete this.H[t.invocationId];try{s(t)}catch(t){this.u.log(e.Error,`Stream callback threw error: ${P(t)}`)}}break}case A.Ping:break;case A.Close:{this.u.log(e.Information,"Close message received from server.");const s=t.error?new Error("Server returned an error on close: "+t.error):void 0;!0===t.allowReconnect?this.connection.stop(s):this.tt=this.et(s);break}default:this.u.log(e.Warning,`Invalid message type: ${t.type}.`)}}this.K()}lt(t){let s,n;try{[n,s]=this.T.parseHandshakeResponse(t)}catch(t){const s="Error parsing handshake response: "+t;this.u.log(e.Error,s);const n=new Error(s);throw this.J(n),n}if(s.error){const t="Server returned handshake error: "+s.error;this.u.log(e.Error,t);const n=new Error(t);throw this.J(n),n}return this.u.log(e.Debug,"Server handshake complete."),this.X(),n}Y(){this.connection.features.inherentKeepAlive||(this.C=(new Date).getTime()+this.keepAliveIntervalInMilliseconds,this.Z())}K(){if(!(this.connection.features&&this.connection.features.inherentKeepAlive||(this.dt=setTimeout((()=>this.serverTimeout()),this.serverTimeoutInMilliseconds),void 0!==this.ft))){let t=this.C-(new Date).getTime();t<0&&(t=0),this.ft=setTimeout((async()=>{if(this.q===R.Connected)try{await this.V(this.M)}catch{this.Z()}}),t)}}serverTimeout(){this.connection.stop(new Error("Server timeout elapsed without receiving a message from the server."))}ut(t){const s=this.D[t.target.toLowerCase()];if(s){try{s.forEach((e=>e.apply(this,t.arguments)))}catch(s){this.u.log(e.Error,`A callback for the method ${t.target.toLowerCase()} threw error '${s}'.`)}if(t.invocationId){const t="Server requested a response, which is not supported in this version of the client.";this.u.log(e.Error,t),this.tt=this.et(new Error(t))}}else this.u.log(e.Warning,`No client method with the name '${t.target}' found.`)}_(t){this.u.log(e.Debug,`HubConnection.connectionClosed(${t}) called while in state ${this.q}.`),this.B=this.B||t||new Error("The underlying connection was closed before the hub handshake could complete."),this.X&&this.X(),this.wt(t||new Error("Invocation canceled due to the underlying connection being closed.")),this.G(),this.Z(),this.q===R.Disconnecting?this.nt(t):this.q===R.Connected&&this.P?this.gt(t):this.q===R.Connected&&this.nt(t)}nt(t){if(this.j){this.q=R.Disconnected,this.j=!1,g.isBrowser&&window.document.removeEventListener("freeze",this.S);try{this.A.forEach((e=>e.apply(this,[t])))}catch(s){this.u.log(e.Error,`An onclose callback called with error '${t}' threw error '${s}'.`)}}}async gt(t){const s=Date.now();let n=0,i=void 0!==t?t:new Error("Attempting to reconnect due to a unknown error."),r=this.yt(n++,0,i);if(null===r)return this.u.log(e.Debug,"Connection not reconnecting because the IRetryPolicy returned null on the first reconnect attempt."),void this.nt(t);if(this.q=R.Reconnecting,t?this.u.log(e.Information,`Connection reconnecting because of error '${t}'.`):this.u.log(e.Information,"Connection reconnecting."),0!==this.R.length){try{this.R.forEach((e=>e.apply(this,[t])))}catch(s){this.u.log(e.Error,`An onreconnecting callback called with error '${t}' threw error '${s}'.`)}if(this.q!==R.Reconnecting)return void this.u.log(e.Debug,"Connection left the reconnecting state in onreconnecting callback. Done reconnecting.")}for(;null!==r;){if(this.u.log(e.Information,`Reconnect attempt number ${n} will start in ${r} ms.`),await new Promise((t=>{this.st=setTimeout(t,r)})),this.st=void 0,this.q!==R.Reconnecting)return void this.u.log(e.Debug,"Connection left the reconnecting state during reconnect delay. Done reconnecting.");try{if(await this.F(),this.q=R.Connected,this.u.log(e.Information,"HubConnection reconnected successfully."),0!==this.U.length)try{this.U.forEach((t=>t.apply(this,[this.connection.connectionId])))}catch(t){this.u.log(e.Error,`An onreconnected callback called with connectionId '${this.connection.connectionId}; threw error '${t}'.`)}return}catch(t){if(this.u.log(e.Information,`Reconnect attempt failed because of error '${t}'.`),this.q!==R.Reconnecting)return this.u.log(e.Debug,`Connection moved to the '${this.q}' from the reconnecting state during reconnect attempt. Done reconnecting.`),void(this.q===R.Disconnecting&&this.nt());i=t instanceof Error?t:new Error(t.toString()),r=this.yt(n++,Date.now()-s,i)}}this.u.log(e.Information,`Reconnect retries have been exhausted after ${Date.now()-s} ms and ${n} failed attempts. Connection disconnecting.`),this.nt()}yt(t,s,n){try{return this.P.nextRetryDelayInMilliseconds({elapsedMilliseconds:s,previousRetryCount:t,retryReason:n})}catch(n){return this.u.log(e.Error,`IRetryPolicy.nextRetryDelayInMilliseconds(${t}, ${s}) threw error '${n}'.`),null}}wt(t){const s=this.H;this.H={},Object.keys(s).forEach((n=>{const i=s[n];try{i(null,t)}catch(s){this.u.log(e.Error,`Stream 'error' callback called with '${t}' threw error: ${P(s)}`)}}))}Z(){this.ft&&(clearTimeout(this.ft),this.ft=void 0)}G(){this.dt&&clearTimeout(this.dt)}at(t,e,s,n){if(s)return 0!==n.length?{arguments:e,streamIds:n,target:t,type:A.Invocation}:{arguments:e,target:t,type:A.Invocation};{const s=this.N;return this.N++,0!==n.length?{arguments:e,invocationId:s.toString(),streamIds:n,target:t,type:A.Invocation}:{arguments:e,invocationId:s.toString(),target:t,type:A.Invocation}}}ct(t,e){if(0!==t.length){e||(e=Promise.resolve());for(const s in t)t[s].subscribe({complete:()=>{e=e.then((()=>this.ht(this.bt(s))))},error:t=>{let n;n=t instanceof Error?t.message:t&&t.toString?t.toString():"Unknown error",e=e.then((()=>this.ht(this.bt(s,n))))},next:t=>{e=e.then((()=>this.ht(this.vt(s,t))))}})}}it(t){const e=[],s=[];for(let n=0;n{let r,o=!1;if(s===W.Text){if(g.isBrowser||g.isWebWorker)r=new this.Pt.EventSource(t,{withCredentials:this.Pt.withCredentials});else{const e=this.$.getCookieString(t),s={};s.Cookie=e;const[n,i]=$();s[n]=i,r=new this.Pt.EventSource(t,{withCredentials:this.Pt.withCredentials,headers:{...s,...this.Pt.headers}})}try{r.onmessage=t=>{if(this.onreceive)try{this.u.log(e.Trace,`(SSE transport) data received. ${y(t.data,this.Pt.logMessageContent)}.`),this.onreceive(t.data)}catch(t){return void this.Ut(t)}},r.onerror=t=>{o?this.Ut():i(new Error("EventSource failed to connect. The connection could not be found on the server, either the connection ID is not present on the server, or a proxy is refusing/buffering the connection. If you have multiple servers check that sticky sessions are enabled."))},r.onopen=()=>{this.u.log(e.Information,`SSE connected to ${this.It}`),this.Nt=r,o=!0,n()}}catch(t){return void i(t)}}else i(new Error("The Server-Sent Events transport only supports the 'Text' transfer format"))}))}async send(t){return this.Nt?b(this.u,"SSE",this.$,this.It,this.St,t,this.Pt):Promise.reject(new Error("Cannot send until the transport is connected"))}stop(){return this.Ut(),Promise.resolve()}Ut(t){this.Nt&&(this.Nt.close(),this.Nt=void 0,this.onclose&&this.onclose(t))}}class X{constructor(t,e,s,n,i,r){this.u=s,this.St=e,this.Lt=n,this.qt=i,this.$=t,this.onreceive=null,this.onclose=null,this.jt=r}async connect(t,s){if(w.isRequired(t,"url"),w.isRequired(s,"transferFormat"),w.isIn(s,W,"transferFormat"),this.u.log(e.Trace,"(WebSockets transport) Connecting."),this.St){const e=await this.St();e&&(t+=(t.indexOf("?")<0?"?":"&")+`access_token=${encodeURIComponent(e)}`)}return new Promise(((n,i)=>{let r;t=t.replace(/^http/,"ws");const o=this.$.getCookieString(t);let h=!1;if(g.isNode){const e={},[s,n]=$();e[s]=n,o&&(e[j.Cookie]=`${o}`),r=new this.qt(t,void 0,{headers:{...e,...this.jt}})}r||(r=new this.qt(t)),s===W.Binary&&(r.binaryType="arraybuffer"),r.onopen=s=>{this.u.log(e.Information,`WebSocket connected to ${t}.`),this.Mt=r,h=!0,n()},r.onerror=t=>{let s=null;s="undefined"!=typeof ErrorEvent&&t instanceof ErrorEvent?t.error:"There was an error with the transport",this.u.log(e.Information,`(WebSockets transport) ${s}.`)},r.onmessage=t=>{if(this.u.log(e.Trace,`(WebSockets transport) data received. ${y(t.data,this.Lt)}.`),this.onreceive)try{this.onreceive(t.data)}catch(t){return void this.Ut(t)}},r.onclose=t=>{if(h)this.Ut(t);else{let e=null;e="undefined"!=typeof ErrorEvent&&t instanceof ErrorEvent?t.error:"WebSocket failed to connect. The connection could not be found on the server, either the endpoint may not be a SignalR endpoint, the connection ID is not present on the server, or there is a proxy blocking WebSockets. If you have multiple servers check that sticky sessions are enabled.",i(new Error(e))}}}))}send(t){return this.Mt&&this.Mt.readyState===this.qt.OPEN?(this.u.log(e.Trace,`(WebSockets transport) sending data. ${y(t,this.Lt)}.`),this.Mt.send(t),Promise.resolve()):Promise.reject("WebSocket is not in the OPEN state")}stop(){return this.Mt&&this.Ut(void 0),Promise.resolve()}Ut(t){this.Mt&&(this.Mt.onclose=()=>{},this.Mt.onmessage=()=>{},this.Mt.onerror=()=>{},this.Mt.close(),this.Mt=void 0),this.u.log(e.Trace,"(WebSockets transport) socket closed."),this.onclose&&(!this.Wt(t)||!1!==t.wasClean&&1e3===t.code?t instanceof Error?this.onclose(t):this.onclose():this.onclose(new Error(`WebSocket closed with status code: ${t.code} (${t.reason||"no reason given"}).`)))}Wt(t){return t&&"boolean"==typeof t.wasClean&&"number"==typeof t.code}}class J{constructor(t,s={}){var n;if(this.Ot=()=>{},this.features={},this.Ft=1,w.isRequired(t,"url"),this.u=void 0===(n=s.logger)?new E(e.Information):null===n?f.instance:void 0!==n.log?n:new E(n),this.baseUrl=this.Bt(t),(s=s||{}).logMessageContent=void 0!==s.logMessageContent&&s.logMessageContent,"boolean"!=typeof s.withCredentials&&void 0!==s.withCredentials)throw new Error("withCredentials option was not a 'boolean' or 'undefined' value");s.withCredentials=void 0===s.withCredentials||s.withCredentials,s.timeout=void 0===s.timeout?1e5:s.timeout;let i=null,r=null;if(g.isNode){const t=require;i=t("ws"),r=t("eventsource")}g.isNode||"undefined"==typeof WebSocket||s.WebSocket?g.isNode&&!s.WebSocket&&i&&(s.WebSocket=i):s.WebSocket=WebSocket,g.isNode||"undefined"==typeof EventSource||s.EventSource?g.isNode&&!s.EventSource&&void 0!==r&&(s.EventSource=r):s.EventSource=EventSource,this.$=s.httpClient||new H(this.u),this.q="Disconnected",this.j=!1,this.Pt=s,this.onreceive=null,this.onclose=null}async start(t){if(t=t||W.Binary,w.isIn(t,W,"transferFormat"),this.u.log(e.Debug,`Starting connection with transfer format '${W[t]}'.`),"Disconnected"!==this.q)return Promise.reject(new Error("Cannot start an HttpConnection that is not in the 'Disconnected' state."));if(this.q="Connecting",this.Xt=this.F(t),await this.Xt,"Disconnecting"===this.q){const t="Failed to start the HttpConnection before stop() was called.";return this.u.log(e.Error,t),await this.tt,Promise.reject(new Error(t))}if("Connected"!==this.q){const t="HttpConnection.startInternal completed gracefully but didn't enter the connection into the connected state!";return this.u.log(e.Error,t),Promise.reject(new Error(t))}this.j=!0}send(t){return"Connected"!==this.q?Promise.reject(new Error("Cannot send data if the connection is not in the 'Connected' State.")):(this.Jt||(this.Jt=new z(this.transport)),this.Jt.send(t))}async stop(t){return"Disconnected"===this.q?(this.u.log(e.Debug,`Call to HttpConnection.stop(${t}) ignored because the connection is already in the disconnected state.`),Promise.resolve()):"Disconnecting"===this.q?(this.u.log(e.Debug,`Call to HttpConnection.stop(${t}) ignored because the connection is already in the disconnecting state.`),this.tt):(this.q="Disconnecting",this.tt=new Promise((t=>{this.Ot=t})),await this.et(t),void await this.tt)}async et(t){this.zt=t;try{await this.Xt}catch(t){}if(this.transport){try{await this.transport.stop()}catch(t){this.u.log(e.Error,`HttpConnection.transport.stop() threw error '${t}'.`),this.Vt()}this.transport=void 0}else this.u.log(e.Debug,"HttpConnection.transport is undefined in HttpConnection.stop() because start() failed.")}async F(t){let s=this.baseUrl;this.St=this.Pt.accessTokenFactory;try{if(this.Pt.skipNegotiation){if(this.Pt.transport!==M.WebSockets)throw new Error("Negotiation can only be skipped when using the WebSocket transport directly.");this.transport=this.Gt(M.WebSockets),await this.Kt(s,t)}else{let e=null,n=0;do{if(e=await this.Qt(s),"Disconnecting"===this.q||"Disconnected"===this.q)throw new Error("The connection was stopped during negotiation.");if(e.error)throw new Error(e.error);if(e.ProtocolVersion)throw new Error("Detected a connection attempt to an ASP.NET SignalR Server. This client only supports connecting to an ASP.NET Core SignalR Server. See https://aka.ms/signalr-core-differences for details.");if(e.url&&(s=e.url),e.accessToken){const t=e.accessToken;this.St=()=>t}n++}while(e.url&&n<100);if(100===n&&e.url)throw new Error("Negotiate redirection limit exceeded.");await this.Yt(s,this.Pt.transport,e,t)}this.transport instanceof F&&(this.features.inherentKeepAlive=!0),"Connecting"===this.q&&(this.u.log(e.Debug,"The HttpConnection connected successfully."),this.q="Connected")}catch(t){return this.u.log(e.Error,"Failed to start the connection: "+t),this.q="Disconnected",this.transport=void 0,this.Ot(),Promise.reject(t)}}async Qt(t){const s={};if(this.St){const t=await this.St();t&&(s[j.Authorization]=`Bearer ${t}`)}const[i,r]=$();s[i]=r;const o=this.Zt(t);this.u.log(e.Debug,`Sending negotiation request: ${o}.`);try{const t=await this.$.post(o,{content:"",headers:{...s,...this.Pt.headers},timeout:this.Pt.timeout,withCredentials:this.Pt.withCredentials});if(200!==t.statusCode)return Promise.reject(new Error(`Unexpected status code returned from negotiate '${t.statusCode}'`));const e=JSON.parse(t.content);return(!e.negotiateVersion||e.negotiateVersion<1)&&(e.connectionToken=e.connectionId),e}catch(t){let s="Failed to complete negotiation with the server: "+t;return t instanceof n&&404===t.statusCode&&(s+=" Either this is not a SignalR endpoint or there is a proxy blocking the connection."),this.u.log(e.Error,s),Promise.reject(new a(s))}}te(t,e){return e?t+(-1===t.indexOf("?")?"?":"&")+`id=${e}`:t}async Yt(t,s,n,i){let r=this.te(t,n.connectionToken);if(this.ee(s))return this.u.log(e.Debug,"Connection was provided an instance of ITransport, using that directly."),this.transport=s,await this.Kt(r,i),void(this.connectionId=n.connectionId);const o=[],h=n.availableTransports||[];let a=n;for(const n of h){const h=this.se(n,s,i);if(h instanceof Error)o.push(`${n.transport} failed:`),o.push(h);else if(this.ee(h)){if(this.transport=h,!a){try{a=await this.Qt(t)}catch(t){return Promise.reject(t)}r=this.te(t,a.connectionToken)}try{return await this.Kt(r,i),void(this.connectionId=a.connectionId)}catch(t){if(this.u.log(e.Error,`Failed to start the transport '${n.transport}': ${t}`),a=void 0,o.push(new c(`${n.transport} failed: ${t}`,M[n.transport])),"Connecting"!==this.q){const t="Failed to select transport before stop() was called.";return this.u.log(e.Debug,t),Promise.reject(new Error(t))}}}}return o.length>0?Promise.reject(new l(`Unable to connect to the server with any of the available transports. ${o.join(" ")}`,o)):Promise.reject(new Error("None of the transports supported by the client are supported by the server."))}Gt(t){switch(t){case M.WebSockets:if(!this.Pt.WebSocket)throw new Error("'WebSocket' is not supported in your environment.");return new X(this.$,this.St,this.u,this.Pt.logMessageContent,this.Pt.WebSocket,this.Pt.headers||{});case M.ServerSentEvents:if(!this.Pt.EventSource)throw new Error("'EventSource' is not supported in your environment.");return new B(this.$,this.St,this.u,this.Pt);case M.LongPolling:return new F(this.$,this.St,this.u,this.Pt);default:throw new Error(`Unknown transport: ${t}.`)}}Kt(t,e){return this.transport.onreceive=this.onreceive,this.transport.onclose=t=>this.Vt(t),this.transport.connect(t,e)}se(t,s,n){const i=M[t.transport];if(null==i)return this.u.log(e.Debug,`Skipping transport '${t.transport}' because it is not supported by this client.`),new Error(`Skipping transport '${t.transport}' because it is not supported by this client.`);if(!function(t,e){return!t||0!=(e&t)}(s,i))return this.u.log(e.Debug,`Skipping transport '${M[i]}' because it was disabled by the client.`),new h(`'${M[i]}' is disabled by the client.`,i);if(!(t.transferFormats.map((t=>W[t])).indexOf(n)>=0))return this.u.log(e.Debug,`Skipping transport '${M[i]}' because it does not support the requested transfer format '${W[n]}'.`),new Error(`'${M[i]}' does not support ${W[n]}.`);if(i===M.WebSockets&&!this.Pt.WebSocket||i===M.ServerSentEvents&&!this.Pt.EventSource)return this.u.log(e.Debug,`Skipping transport '${M[i]}' because it is not supported in your environment.'`),new o(`'${M[i]}' is not supported in your environment.`,i);this.u.log(e.Debug,`Selecting transport '${M[i]}'.`);try{return this.Gt(i)}catch(t){return t}}ee(t){return t&&"object"==typeof t&&"connect"in t}Vt(t){if(this.u.log(e.Debug,`HttpConnection.stopConnection(${t}) called while in state ${this.q}.`),this.transport=void 0,t=this.zt||t,this.zt=void 0,"Disconnected"!==this.q){if("Connecting"===this.q)throw this.u.log(e.Warning,`Call to HttpConnection.stopConnection(${t}) was ignored because the connection is still in the connecting state.`),new Error(`HttpConnection.stopConnection(${t}) was called while the connection is still in the connecting state.`);if("Disconnecting"===this.q&&this.Ot(),t?this.u.log(e.Error,`Connection disconnected with error '${t}'.`):this.u.log(e.Information,"Connection disconnected."),this.Jt&&(this.Jt.stop().catch((t=>{this.u.log(e.Error,`TransportSendQueue.stop() threw error '${t}'.`)})),this.Jt=void 0),this.connectionId=void 0,this.q="Disconnected",this.j){this.j=!1;try{this.onclose&&this.onclose(t)}catch(s){this.u.log(e.Error,`HttpConnection.onclose(${t}) threw error '${s}'.`)}}}else this.u.log(e.Debug,`Call to HttpConnection.stopConnection(${t}) was ignored because the connection is already in the disconnected state.`)}Bt(t){if(0===t.lastIndexOf("https://",0)||0===t.lastIndexOf("http://",0))return t;if(!g.isBrowser)throw new Error(`Cannot resolve '${t}'.`);const s=window.document.createElement("a");return s.href=t,this.u.log(e.Information,`Normalizing '${t}' to '${s.href}'.`),s.href}Zt(t){const e=t.indexOf("?");let s=t.substring(0,-1===e?t.length:e);return"/"!==s[s.length-1]&&(s+="/"),s+="negotiate",s+=-1===e?"":t.substring(e),-1===s.indexOf("negotiateVersion")&&(s+=-1===e?"?":"&",s+="negotiateVersion="+this.Ft),s}}class z{constructor(t){this.ne=t,this.ie=[],this.re=!0,this.oe=new V,this.he=new V,this.ce=this.ae()}send(t){return this.le(t),this.he||(this.he=new V),this.he.promise}stop(){return this.re=!1,this.oe.resolve(),this.ce}le(t){if(this.ie.length&&typeof this.ie[0]!=typeof t)throw new Error(`Expected data to be of type ${typeof this.ie} but was of type ${typeof t}`);this.ie.push(t),this.oe.resolve()}async ae(){for(;;){if(await this.oe.promise,!this.re){this.he&&this.he.reject("Connection stopped.");break}this.oe=new V;const t=this.he;this.he=void 0;const e="string"==typeof this.ie[0]?this.ie.join(""):z.ue(this.ie);this.ie.length=0;try{await this.ne.send(e),t.resolve()}catch(e){t.reject(e)}}}static ue(t){const e=t.map((t=>t.byteLength)).reduce(((t,e)=>t+e)),s=new Uint8Array(e);let n=0;for(const e of t)s.set(new Uint8Array(e),n),n+=e.byteLength;return s.buffer}}class V{constructor(){this.promise=new Promise(((t,e)=>[this.de,this.fe]=[t,e]))}resolve(){this.de()}reject(t){this.fe(t)}}class G{constructor(){this.name="json",this.version=1,this.transferFormat=W.Text}parseMessages(t,s){if("string"!=typeof t)throw new Error("Invalid input for JSON hub protocol. Expected a string.");if(!t)return[];null===s&&(s=f.instance);const n=D.parse(t),i=[];for(const t of n){const n=JSON.parse(t);if("number"!=typeof n.type)throw new Error("Invalid payload.");switch(n.type){case A.Invocation:this.pe(n);break;case A.StreamItem:this.we(n);break;case A.Completion:this.ge(n);break;case A.Ping:case A.Close:break;default:s.log(e.Information,"Unknown message type '"+n.type+"' ignored.");continue}i.push(n)}return i}writeMessage(t){return D.write(JSON.stringify(t))}pe(t){this.ye(t.target,"Invalid payload for Invocation message."),void 0!==t.invocationId&&this.ye(t.invocationId,"Invalid payload for Invocation message.")}we(t){if(this.ye(t.invocationId,"Invalid payload for StreamItem message."),void 0===t.item)throw new Error("Invalid payload for StreamItem message.")}ge(t){if(t.result&&t.error)throw new Error("Invalid payload for Completion message.");!t.result&&t.error&&this.ye(t.error,"Invalid payload for Completion message."),this.ye(t.invocationId,"Invalid payload for Completion message.")}ye(t,e){if("string"!=typeof t||""===t)throw new Error(e)}}const K={trace:e.Trace,debug:e.Debug,info:e.Information,information:e.Information,warn:e.Warning,warning:e.Warning,error:e.Error,critical:e.Critical,none:e.None};class Q{configureLogging(t){if(w.isRequired(t,"logging"),void 0!==t.log)this.logger=t;else if("string"==typeof t){const e=function(t){const e=K[t.toLowerCase()];if(void 0!==e)return e;throw new Error(`Unknown log level: ${t}`)}(t);this.logger=new E(e)}else this.logger=new E(t);return this}withUrl(t,e){return w.isRequired(t,"url"),w.isNotEmpty(t,"url"),this.url=t,this.httpConnectionOptions="object"==typeof e?{...this.httpConnectionOptions,...e}:{...this.httpConnectionOptions,transport:e},this}withHubProtocol(t){return w.isRequired(t,"protocol"),this.protocol=t,this}withAutomaticReconnect(t){if(this.reconnectPolicy)throw new Error("A reconnectPolicy has already been set.");return t?Array.isArray(t)?this.reconnectPolicy=new q(t):this.reconnectPolicy=t:this.reconnectPolicy=new q,this}build(){const t=this.httpConnectionOptions||{};if(void 0===t.logger&&(t.logger=this.logger),!this.url)throw new Error("The 'HubConnectionBuilder.withUrl' method must be called before building the connection.");const e=new J(this.url,t);return N.create(e,this.logger||f.instance,this.protocol||new G,this.reconnectPolicy)}}return Uint8Array.prototype.indexOf||Object.defineProperty(Uint8Array.prototype,"indexOf",{value:Array.prototype.indexOf,writable:!0}),Uint8Array.prototype.slice||Object.defineProperty(Uint8Array.prototype,"slice",{value:function(t,e){return new Uint8Array(Array.prototype.slice.call(this,t,e))},writable:!0}),Uint8Array.prototype.forEach||Object.defineProperty(Uint8Array.prototype,"forEach",{value:Array.prototype.forEach,writable:!0}),s})()},"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.signalR=e():t.signalR=e(); 2 | //# sourceMappingURL=signalr.min.js.map --------------------------------------------------------------------------------