Skip to main content

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

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:

services/aps.js
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.

services/aps.js
// ACC Assue APIs

//export issues list of the project
service.getIssues = async (projectId, token) => {
let allIssues = [];
let offset = 0;
let totalResults = 0;
do {
const resp = await issuesClient.getIssues(projectId, { accessToken: token, offset: offset });
console.log(`Fetched ${resp.results.length} issues from offset ${offset}`);
allIssues = allIssues.concat(resp.results);
offset += resp.pagination.limit;
totalResults = resp.pagination.totalResults;
} while (offset < totalResults)
return allIssues;
};

Pagination

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.

tip
  • If the number of issue records is large, the process may take some time. In your production application, consider adding a progress bar that displays the current offset and limit, or use query parameters filter to request only specific issues from the API.
  • This sample retrieves all issue data. In a real application, you can request only specific fields by using the fields query parameter.

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.

Append the following code to the end of the services/aps.js file.

services/aps.js

//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 });
console.log(`created issue with id ${resp.id} from csv row ${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 });
console.log(`modified issue with id ${resp.id} from csv row ${oneIssueData.csvRowNum}`);
}

} catch (e) {
results.failed.push({ csvRowNum: oneIssueData.csvRowNum, reason: e.toString() });
console.log(`failed to import issue from csv row ${oneIssueData.csvRowNum} due to ${e.toString()}`);
}
}));

return results;
};

info

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.

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:

routes/issues.js
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:

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}...`));

Try it out

And that's it for the server side. Time to try it out!

Issues Response

  • POST Issues: Browsers by default do not provide built-in tools to test endpoints that modify data, such as POST, PATCH, DELETE, etc. To test the creation or modification of issues, use other HTTP tools or command-line scripts. Below is a demo using Postman.

    • endpoint of this call POST: http://localhost:8080/api/issues/issues

    • Input headers Content-Type and Cookie. Check Try it Out of Authentication to get Cookie value

      • Content-Type:application/json

      • NodeJS: Cookie:session=< value >;session.sig=< value >

      • NET: Cookie:internal_token=< value >;refresh_token=< value >;expires_at=< value>

        Write Test Header

    • Input request payload. If new issue, no id. The issueSubtypeId can be copied to any of the records of GET Issues. If with id, that means to update existing issue.

          {
      "projectId":"854c194f-05b3-4117-b926-69fa36c33c0c",
      "data":[
      {
      "title":"create new issue by my application",
      "issueSubtypeId":"a1637225-d69c-4afc-ad02-1c73a92ee3c1",
      "status":"open"
      },
      {
      "id": "b4f6e2f7-7bda-4e87-b139-7d94bc9c2a8d"
      "title":"modify an existing issue by my application",
      "status":"close"
      }
      ]
      }