Project Issues
Now, we will implement our server to consume ACC Issues APIs to fetch project issues and create/modify issues.
Work with Issues
We shall be utilizing ACC Issue SDK in both NodeJs and .NET samples
- Node.js & VSCode
- .NET & VSCode
- .NET & VS2022
Add the @aps_sdk/construction-issues
library, and also create an instance of
SDK client of IssuesClient
at the beginning of services/aps.js
file:
const { SdkManagerBuilder } = require('@aps_sdk/autodesk-sdkmanager');
const { AuthenticationClient, Scopes, ResponseType } = require('@aps_sdk/authentication');
const { DataManagementClient } = require('@aps_sdk/data-management');
const { IssuesClient } = require('@aps_sdk/construction-issues');
const { APS_CLIENT_ID, APS_CLIENT_SECRET, APS_CALLBACK_URL, INTERNAL_TOKEN_SCOPES, PUBLIC_TOKEN_SCOPES } = require('../config.js');
const service = module.exports = {};
const sdk = SdkManagerBuilder.create().build();
const authenticationClient = new AuthenticationClient(sdk);
const dataManagementClient = new DataManagementClient(sdk);
const issuesClient = new IssuesClient(sdk);
Extract Issues
Next, we will add a couple of helper functions to extract issues. Append the following code to the end of the services/aps.js
file.
We will extract 100 issues in one call and move to the next 100 issues until all issues are fetched.
// ACC Assue APIs
//Extract Issue Data
service.getIssues = async (projectId, token) => {
let allIssues = [];
let offset = 0;
let totalResults = 0;
do{
const resp = await issuesClient.getIssues(projectId, {accessToken:token,offset:offset});
allIssues = allIssues.concat(resp.results);
offset += resp.pagination.limit;
totalResults = resp.pagination.totalResults;
}while (offset < totalResults)
return allIssues;
};
Create a APS.Issues.cs
under the Models
subfolder with the following content:
using System.Collections.Generic;
using System.Threading.Tasks;
using Autodesk.Construction.Issues;
using Autodesk.Construction.Issues.Model;
using Newtonsoft.Json.Linq;
public partial class APS
{
//export issues list of the project
public async Task<IEnumerable<dynamic>> GetIssues(string projectId, Tokens tokens)
{
IssuesClient issueClient = new IssuesClient(_SDKManager);
var allIssues = new List<Autodesk.Construction.Issues.Model.Results>();
var offset = 0;
var totalResult = 0;
do
{
var issues = await issueClient.GetIssuesAsync(projectId, accessToken: tokens.InternalToken, offset: offset);
allIssues.AddRange(issues.Results);
offset += (int)issues.Pagination.Limit;
totalResult = (int)issues.Pagination.TotalResults;
} while (offset < totalResult);
return allIssues;
}
}
Create a APS.Issues.cs
under the Models
subfolder with the following content:
using System.Collections.Generic;
using System.Threading.Tasks;
using Autodesk.Construction.Issues;
using Autodesk.Construction.Issues.Model;
using Newtonsoft.Json.Linq;
public partial class APS
{
//export issues list of the project
public async Task<IEnumerable<dynamic>> GetIssues(string projectId, Tokens tokens)
{
IssuesClient issueClient = new IssuesClient(_SDKManager);
var allIssues = new List<Autodesk.Construction.Issues.Model.Results>();
var offset = 0;
var totalResult = 0;
do
{
var issues = await issueClient.GetIssuesAsync(projectId, accessToken: tokens.InternalToken, offset: offset);
allIssues.AddRange(issues.Results);
offset += (int)issues.Pagination.Limit;
totalResult = (int)issues.Pagination.TotalResults;
} while (offset < totalResult);
return allIssues;
}
}
The 'Issues' collection is managed in the APS cloud database. To ensure optimal performance, the API follows the web standard of pagination meaning each HTTP request returns only a portion of the records in the collection. With current design, ACC Issue API returns 1-100 issues in one call by default. We also have chance to specify the limit parameter which indicates how many records in one page. The other parameter offset specifies from which index of issue to extract the records. Check API reference of GET:Issues for more information.
Import Issues
Moving forward, we will add helper functions to create and modify issues. POST Issues for creating a new issue, PATCH Issues/:IssueId for modifying an existing issue. When the server receives records imported from the client (via CSV), it checks whether a record contains an ID value. If not, it creates a new issue. If an ID exists, it updates the corresponding issue with the new data.
To track status, a JSON array is logged on console with the successfully created or modified issues, along with any failed API calls and their corresponding CSV row numbers.
- Node.js & VSCode
- .NET & VSCode
- .NET & VS2022
Append the following code to the end of the services/aps.js file.
//import issues (create new issue or modify existing issue)
service.createOrModifyIssues = async (projectId,token,data) => {
let results = {
created:[],
modified:[],
failed:[]
}
await Promise.all(
data.map(async (oneIssueData)=>{
try{
//remove unsupported fields and build the payload
const {id, csvRowNum, ...payload } = oneIssueData;
if(id == '' || id==undefined || id==null){
//create new issue
const resp = await issuesClient.createIssue(projectId,payload,{accessToken:token});
results.created.push({id:resp.id,csvRowNum:oneIssueData.csvRowNum});
}else{
//modify an issue
const resp = await issuesClient.patchIssueDetails(projectId,id,payload,{accessToken:token});
results.modified.push({id:resp.id,csvRowNum:oneIssueData.csvRowNum});
}
}catch(e){
results.failed.push({csvRowNum:oneIssueData.csvRowNum,reason:e.toString()});
}
}));
return results;
};
Add the following content to APS.Issues.cs
file
//import issues (create new issue or modify existing issue)
public async Task<TaskRes> CreateOrModifyACCIssues(string projectId, Tokens tokens, JArray body)
{
IssuesClient issueClient = new IssuesClient(_SDKManager);
var taskRes = new TaskRes()
{
created = new List<succeded>(),
modified = new List<succeded>(),
failed = new List<failed>()
};
foreach (JToken eachItem in body)
{
Autodesk.Construction.Issues.Model.Results issue =
eachItem.ToObject<Autodesk.Construction.Issues.Model.Results>();
try
{
//some attributes are enum with IssuePayload
//value of Autodesk.Construction.Issues.Model.Results is string
//need to convert to enum.
Status status = (Status)Enum.Parse(typeof(Status), CapitalizeFirstLetter(issue.Status));
AssignedToType assignedToType = (AssignedToType)Enum.Parse(typeof(AssignedToType), CapitalizeFirstLetter(issue.AssignedToType));
IssuePayload issuePayload = new IssuePayload
{
Title = issue.Title,
Description = issue.Description,
Status = status,
IssueSubtypeId = issue.IssueSubtypeId,
DueDate = issue.DueDate,
AssignedTo = issue.AssignedTo,
AssignedToType = AssignedToType.User,
RootCauseId = issue.RootCauseId,
Published = issue.Published
};
if ((string)eachItem["id"] == null || (string)eachItem["id"] == "")
{
//create new issue
Issue res = await issueClient.CreateIssueAsync(projectId, issuePayload, accessToken: tokens.InternalToken);
taskRes.created.Add(new succeded { id = res.Id, csvRowNum = (string)eachItem["csvRowNum"] });
}
else
{
//modify issue
Issue res = await issueClient.PatchIssueDetailsAsync(projectId, issue.Id, issuePayload, accessToken: tokens.InternalToken);
taskRes.modified.Add(new succeded { id = res.Id, csvRowNum = (string)eachItem["csvRowNum"] });
}
}
catch (Exception e)
{
taskRes.failed.Add(new failed { csvRowNum = (string)eachItem["csvRowNum"], reason = e.ToString() });
}
}
return taskRes;
}
public class TaskRes
{
public List<succeded> created { get; set; }
public List<succeded> modified { get; set; }
public List<failed> failed { get; set; }
}
public class succeded
{
public string id { get; set; }
public string csvRowNum { get; set; }
}
public class failed
{
public string csvRowNum { get; set; }
public string reason { get; set; }
}
public static string CapitalizeFirstLetter(string input)
{
if (string.IsNullOrEmpty(input))
{
return input; // Return the input if it's null or empty
}
// Capitalize the first letter and make the rest lowercase
return char.ToUpper(input[0]) + input.Substring(1).ToLower();
}
Add the following content to APS.Issues.cs
file
//import issues (create new issue or modify existing issue)
public async Task<TaskRes> CreateOrModifyACCIssues(string projectId, Tokens tokens, JArray body)
{
IssuesClient issueClient = new IssuesClient(_SDKManager);
var taskRes = new TaskRes()
{
created = new List<succeded>(),
modified = new List<succeded>(),
failed = new List<failed>()
};
foreach (JToken eachItem in body)
{
Autodesk.Construction.Issues.Model.Results issue =
eachItem.ToObject<Autodesk.Construction.Issues.Model.Results>();
try
{
//some attributes are enum with IssuePayload
//value of Autodesk.Construction.Issues.Model.Results is string
//need to convert to enum.
Status status = (Status)Enum.Parse(typeof(Status), CapitalizeFirstLetter(issue.Status));
AssignedToType assignedToType = (AssignedToType)Enum.Parse(typeof(AssignedToType), CapitalizeFirstLetter(issue.AssignedToType));
IssuePayload issuePayload = new IssuePayload
{
Title = issue.Title,
Description = issue.Description,
Status = status,
IssueSubtypeId = issue.IssueSubtypeId,
DueDate = issue.DueDate,
AssignedTo = issue.AssignedTo,
AssignedToType = AssignedToType.User,
RootCauseId = issue.RootCauseId,
Published = issue.Published
};
if ((string)eachItem["id"] == null || (string)eachItem["id"] == "")
{
//create new issue
Issue res = await issueClient.CreateIssueAsync(projectId, issuePayload, accessToken: tokens.InternalToken);
taskRes.created.Add(new succeded { id = res.Id, csvRowNum = (string)eachItem["csvRowNum"] });
}
else
{
//modify issue
Issue res = await issueClient.PatchIssueDetailsAsync(projectId, issue.Id, issuePayload, accessToken: tokens.InternalToken);
taskRes.modified.Add(new succeded { id = res.Id, csvRowNum = (string)eachItem["csvRowNum"] });
}
}
catch (Exception e)
{
taskRes.failed.Add(new failed { csvRowNum = (string)eachItem["csvRowNum"], reason = e.ToString() });
}
}
return taskRes;
}
public class TaskRes
{
public List<succeded> created { get; set; }
public List<succeded> modified { get; set; }
public List<failed> failed { get; set; }
}
public class succeded
{
public string id { get; set; }
public string csvRowNum { get; set; }
}
public class failed
{
public string csvRowNum { get; set; }
public string reason { get; set; }
}
public static string CapitalizeFirstLetter(string input)
{
if (string.IsNullOrEmpty(input))
{
return input; // Return the input if it's null or empty
}
// Capitalize the first letter and make the rest lowercase
return char.ToUpper(input[0]) + input.Substring(1).ToLower();
}
The issue creation and modification APIs only accept a limited set of fields in the request payload. This sample demonstrates how to just use some required fields from the CSV data.
- title
- description
- issueSubtypeId
- status
- dueDate
- assignedTo
- assignedToType
- rootCauseId
- published
Server endpoints
Next, let's expose the routings to extract issues and import issue to the client-side code through another set of endpoints.
- Node.js & VSCode
- .NET & VSCode
- .NET & VS2022
Next, let's expose the routings to extract and import issues to the client-side code through set of endpoints.
Create issues.js
file under the routes subfolder with the following content:
const express = require('express');
var bodyParser = require('body-parser');
const { authRefreshMiddleware,
getIssues,
createOrModifyIssues
} = require('../services/aps.js');
let router = express.Router();
router.use(authRefreshMiddleware);
//get issues
router.get('/api/issues/issues', async function(req, res, next){
try {
const issues = await getIssues(req.query.projectId,req.internalOAuthToken.access_token);
res.json(issues);
} catch (err) {
next(err);
}
});
//create new issue or modify issue
router.post('/api/issues/issues', bodyParser.json(), async function (req, res, next) {
const projectId = req.body.projectId;
const issues = req.body.data;
try {
const importResults = await createOrModifyIssues(projectId,req.internalOAuthToken.access_token,issues);
res.json(importResults);
} catch (err) {
next(err);
}
});
module.exports = router;
And mount the router to our server application by modifying server.js
:
const express = require('express');
const session = require('cookie-session');
const { PORT, SERVER_SESSION_SECRET } = require('./config.js');
let app = express();
app.use(express.static('wwwroot'));
app.use(session({ secret: SERVER_SESSION_SECRET, maxAge: 24 * 60 * 60 * 1000 }));
app.use(require('./routes/auth.js'));
app.use(require('./routes/hubs.js'));
app.use(require('./routes/issues.js'));
app.listen(PORT, () => console.log(`Server listening on port ${PORT}...`));
Create a IssuesController.cs
file under the Controllers
subfolder with the following content:
using System.Threading.Tasks;
using aps_acc_issues_dotnet.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
[ApiController]
[Route("api/[controller]")]
public class IssuesController : ControllerBase
{
private readonly ILogger<IssuesController> _logger;
private readonly APS _aps;
private JsonSerializerSettings settings = new JsonSerializerSettings
{
ContractResolver = new ForceIncludeNullsResolver(),
NullValueHandling = NullValueHandling.Include
};
public IssuesController(ILogger<IssuesController> logger, APS aps)
{
_logger = logger;
_aps = aps;
}
[HttpGet("issues")]
public async Task<ActionResult<string>> ListIssues(string projectId)
{
var tokens = await AuthController.PrepareTokens(Request, Response, _aps);
if (tokens == null)
{
return Unauthorized();
}
var issues = await _aps.GetIssues(Request.Query["projectId"], tokens);
return JsonConvert.SerializeObject(issues,settings);
}
//create new issue or modify issue
[HttpPost("issues")]
public async Task<ActionResult> CreateOrModifyIssues([FromBody] JObject content)
{
var tokens = await AuthController.PrepareTokens(Request, Response, _aps);
if (tokens == null)
{
return Unauthorized();
}
string projectId = content["projectId"].Value<string>();
dynamic issues = content["data"].Value<dynamic>();
var status = await _aps.CreateOrModifyACCIssues(projectId, tokens, issues);
return Ok(new { created = status.created, modified = status.modified, failed = status.failed });
}
}
//include the fields whose value =null when JsonConvert.SerializeObject
public class ForceIncludeNullsResolver : DefaultContractResolver
{
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
var prop = base.CreateProperty(member, memberSerialization);
// Always include properties, even if they're null
prop.NullValueHandling = NullValueHandling.Include;
prop.DefaultValueHandling = DefaultValueHandling.Include;
return prop;
}
}
Create a IssuesController.cs
file under the Controllers
subfolder with the following content:
using System.Threading.Tasks;
using aps_acc_issues_dotnet.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
[ApiController]
[Route("api/[controller]")]
public class IssuesController : ControllerBase
{
private readonly ILogger<IssuesController> _logger;
private readonly APS _aps;
private JsonSerializerSettings settings = new JsonSerializerSettings
{
ContractResolver = new ForceIncludeNullsResolver(),
NullValueHandling = NullValueHandling.Include
};
public IssuesController(ILogger<IssuesController> logger, APS aps)
{
_logger = logger;
_aps = aps;
}
[HttpGet("issues")]
public async Task<ActionResult<string>> ListIssues(string projectId)
{
var tokens = await AuthController.PrepareTokens(Request, Response, _aps);
if (tokens == null)
{
return Unauthorized();
}
var issues = await _aps.GetIssues(Request.Query["projectId"], tokens);
return JsonConvert.SerializeObject(issues,settings);
}
//create new issue or modify issue
[HttpPost("issues")]
public async Task<ActionResult> CreateOrModifyIssues([FromBody] JObject content)
{
var tokens = await AuthController.PrepareTokens(Request, Response, _aps);
if (tokens == null)
{
return Unauthorized();
}
string projectId = content["projectId"].Value<string>();
dynamic issues = content["data"].Value<dynamic>();
var status = await _aps.CreateOrModifyACCIssues(projectId, tokens, issues);
return Ok(new { created = status.created, modified = status.modified, failed = status.failed });
}
}
//include the fields whose value =null when JsonConvert.SerializeObject
public class ForceIncludeNullsResolver : DefaultContractResolver
{
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
var prop = base.CreateProperty(member, memberSerialization);
// Always include properties, even if they're null
prop.NullValueHandling = NullValueHandling.Include;
prop.DefaultValueHandling = DefaultValueHandling.Include;
return prop;
}
}
Try it out
And that's it for the server side. Time to try it out!
- Use same project ID in previous step (remove b.) and try to call the endpoint: http://localhost:8080/api/issues/issues?projectId={your-project-id}, the server application should respond with a JSON list of all the issues from this project.
- It would take a bit long time if the volume of the issue records is large. This sample extracts raw data of issue.
- Some fields are json object or array object. Their value will be dumped as string in this sample.
- To test creating or modifying issues, use HTTP test tools or commandline scripts to perform the test with access token and request payload.Recommended HTTP test tools: Postman, Insomnia