Know when a Milestone is Created
This code sample demonstrates the use of the Manufacturing Data Model API to subscribe to a webhook event, and how to handle the Callback. It takes the name of a hub, the name of a project with the hub, and the name of a design (component) as inputs. Thereafter it subscribes to a MILESTONE_CREATED event on a specific component in that design.
Before You Begin
- If you do not have an app registered for the Manufacturing Data Model API, follow the procedure outlined in Create an App to sign up for an APS account (if required) and obtain a Client ID for your app. When specifying details of the app:
- Specify
http://localhost:3000/callback/oauth
as the Callback URL. - Verify that the Manufacturing Data Model API and Webhooks API are selected under API Access.
- Specify
- Install
ngrok
.ngrok
allows you to expose a web server running on your local machine to the Internet. For installation instructions, see the ngrok documentation. - Clone or fork the Git Repository.
- In the 2.Know When a New Milestone is Available folder, install all dependencies by running:
npm i
Setting up the Application
index.js:
// Replace the string literal values with your own client id, client secret,
// hub name, project name and component name.
const clientId = '<YOUR_CLIENT_ID>';
const clientSecret = '<YOUR_CLIENT_SECRET>';
const hubName = '<YOUR_HUB_NAME>';
const projectName = '<YOUR_PROJECT_NAME>';
const componentName = '<YOUR_COMPONENT_NAME>';
const eventType = 'MILESTONE_CREATED';
- In index.js shown above, specify values for the following variables:
clientId
- Your Client IDclientSecret
- Your Client SecrethubName
- The name of the hub that contains the component you want to monitor.projectName
- The name of the project that contains the component you want to monitor.componentName
- The name of the design (component) that contains the component you want to monitor.
- In the terminal, start ngrok with the following command:
ngrok http 3000 -host-header="localhost:3000"
- Copy the ngrok URL that is returned in the terminal, and paste it as the value of the
ngrokUrl
constant in the file index.js.
![]()
index.js:
// In a terminal, start ngrok with the following command: // ngrok http 3000 -host-header="localhost:3000" // Copy and paste ngrok URL value returned to your terminal console. const ngrokUrl = '<YOUR_NGROK_URL>';
Running the Sample
- In the terminal, navigate to the 2.Know When a New Milestone is Available folder and execute the following command:
npm start
- When prompted, open a browser and go to http://localhost:3000
- Once the “Got the access token. You can close the browser!” message is displayed on the browser, return to the terminal.
- Use Autodesk Fusion, and create a milestone for the component you are monitoring. In about 2-3 minutes, the terminal should display a response similar to the response shown in the following code-block:
Open http://localhost:3000 in a web browser in order to log in with your Autodesk account!
Deleted webhook 75a2adac-622e-41a0-b560-8fdb4a5228bd
Created webhook c3bebb52-4224-45ff-ad5b-fd7353f824a6
Listening to the events on http://localhost:3000 => https://fc4d-86-2-185-49.ngrok.io/callback
Create a milestone in Autodesk Fusion and wait for the event to be listed here:
Received a notification with following content:
{
"id": "fd3bd89e-201e-4824-867a-7543a3b47e73",
"source": "urn:com.autodesk.forge:clientid:cF2pIBIYqEpzS0d1gEicgU0nR0acgrjG",
"specversion": "1.0",
"type": "autodesk.data.pim:milestone.created-1.0.0",
"subject": "urn:autodesk.data.pim:component:Y29tcH5jby4xMDY5RUZIZ1FreWVUaExVLWg0M0RBfkhJeExwelBKdWpvcFRvc24zZVRtN2lfYWdhfn4",
"time": "2022-05-09T16:00:17.287Z",
"data": {
"componentid": "Y29tcH5jby4xMDY5RUZIZ1FreWVUaExVLWg0M0RBfkhJeExwelBKdWpvcFRvc24zZVRtN2lfYWdhfn4",
"milestonename": "Milestone V48",
"eventtype": "MILESTONE_CREATED"
},
"dataschema": "https://forge.autodesk.com/schemas/pim-event-schema-v1.0.0.json",
"traceparent": "00-5d062bba301551d19e86db826d135397-e440e5a6d791d97c-01"
}
Workflow Description
The workflow can be summarized in two steps:
- Get the ID of root component
- Subscribe to the MILESTONE_CREATED event on the component
- Listen to the event
The file app.js sends a mutation to the GraphQL API to create a webhook.
app.js:
async subscribeToEvent(hubName, projectName, componentName, eventType) {
try {
let rootComponentId = await this.getRootComponentId(hubName, projectName, componentName);
let response = await this.sendQuery(
`mutation CreateWebhook($componentId: ID!, $eventType: WebhookEventTypeEnum!, $callbackURL: String!) {
createWebhook(webhook: {
componentId: $componentId,
eventType: $eventType,
callbackURL: $callbackURL,
expiresOn: "2022-10-12T07:20:50.52Z",
secretToken: "12345678901234567890123456789012"
}) {
id
}
}`,
{
componentId: rootComponentId,
eventType,
callbackURL: this.callbackUrl
}
)
let webhookId = response.data.data.createWebhook.id;
console.log('Created webhook ' + webhookId + ' for component ' + rootComponentId);
return webhookId;
} catch (err) {
console.log("There was an issue: " + err.message)
}
}
Thereafter it starts listening for the Callback
app.js:
async startMonitoringEvents() {
try {
console.log(
`Listening to the events on http://localhost:${this.port} => ${this.callbackUrl}`
);
app.post(this.callbackPath, async (req, res) => {
// Format the json string content to make it easier to read
let formatted = JSON.stringify(req.body, null, 2);
console.log(`Received a notification with following content:\n${formatted}`);
res.status(200).end();
});
app.listen(this.port);
} catch (error) {
console.log(error);
}
};
JavaScript Source Code Walkthrough
app.js
This is the main operational code of the application. It sends a GraphQL mutation to subscribe to a MILESTONE_CREATED event. It also contains the code to listen to the Callback. It also contains the code to list and delete webhooks.
// Axios is a promise-based HTTP client for the browser and node.js.
import axios from "axios";
// Express is a JavaSscript web application framework.
import express from "express";
// Instantiate an express application
let app = express();
// Use a json handler for incoming HTTP requests.
app.use(express.json());
// Application constructor
export default class App {
constructor(accessToken, ngrokUrl) {
this.graphAPI = 'https://developer.api.autodesk.com/fusiondata/2022-04/graphql';
this.accessToken = accessToken;
this.port = 3000;
this.callbackPath = '/callback';
this.callbackUrl = ngrokUrl + this.callbackPath;
}
getRequestHeaders() {
return {
"Content-type": "application/json",
"Authorization": "Bearer " + this.accessToken,
};
}
async sendQuery(query, variables) {
let response = await axios({
method: 'POST',
url: `${this.graphAPI}`,
headers: this.getRequestHeaders(),
data: {
query,
variables
}
})
if (response.data.errors) {
let formatted = JSON.stringify(response.data.errors, null, 2);
console.log(`API error:\n${formatted}`);
}
return response;
}
// <subscribeToEvent>
async subscribeToEvent(hubName, projectName, componentName, eventType) {
try {
let rootComponentId = await this.getRootComponentId(hubName, projectName, componentName);
let response = await this.sendQuery(
`mutation CreateWebhook($componentId: ID!, $eventType: WebhookEventTypeEnum!, $callbackURL: String!) {
createWebhook(webhook: {
componentId: $componentId,
eventType: $eventType,
callbackURL: $callbackURL,
expiresOn: "2022-10-12T07:20:50.52Z",
secretToken: "12345678901234567890123456789012"
}) {
id
}
}`,
{
componentId: rootComponentId,
eventType,
callbackURL: this.callbackUrl
}
)
let webhookId = response.data.data.createWebhook.id;
console.log('Created webhook ' + webhookId + ' for component ' + rootComponentId);
return webhookId;
} catch (err) {
console.log("There was an issue: " + err.message)
}
}
// </subscribeToEvent>
// <unsubscribeToEvent>
async unsubscribeToEvent(eventType) {
try {
let webhooks = await this.getWebhooks(eventType);
for (let webhook of webhooks) {
let response = await this.sendQuery(
`mutation DeleteWebhook($webhookId: String!) {
deleteWebhook(webhookId: $webhookId)
}`,
{
webhookId: webhook.id
}
)
console.log('Deleted webhook ' + response.data.data.deleteWebhook);
}
} catch (err) {
console.log("There was an issue: " + err.message)
}
}
// </unsubscribeToEvent>
getComponent(response, hubName, projectName, componentName) {
let hubs = response.data.data.hubs.results;
if (hubs.length < 1)
throw { message: `Hub "${hubName}" does not exist` }
let projects = hubs[0].projects.results;
if (projects.length < 1)
throw { message: `Project "${projectName}" does not exist` }
let files = projects[0].rootFolder.items.results;
if (files.length < 1)
throw { message: `Component "${componentName}" does not exist` }
return files[0];
}
async getRootComponentId(hubName, projectName, componentName) {
let response = await this.sendQuery(
`query GetRootComponent($hubName: String!, $projectName: String!, $componentName: String!) {
hubs(filter:{name:$hubName}) {
results {
name
projects(filter:{name:$projectName}) {
results {
name
rootFolder {
items(filter:{name:$componentName}) {
results {
... on Component {
id
}
}
}
}
}
}
}
}
}`,
{
hubName,
projectName,
componentName
}
)
let rootComponent = this.getComponent(
response, hubName, projectName, componentName
);
return rootComponent.id;
}
async getWebhooks(eventType) {
let response = await this.sendQuery(
`query GetWebhooks {
webhooks {
results {
id
eventType
}
}
}`
)
let webhooks = response.data.data
.webhooks.results.filter(webhook => {
return (
webhook.eventType === eventType
)
});
return webhooks;
}
// <startMonitoringEvents>
async startMonitoringEvents() {
try {
console.log(
`Listening to the events on http://localhost:${this.port} => ${this.callbackUrl}`
);
app.post(this.callbackPath, async (req, res) => {
// Format the json string content to make it easier to read
let formatted = JSON.stringify(req.body, null, 2);
console.log(`Received a notification with following content:\n${formatted}`);
res.status(200).end();
});
app.listen(this.port);
} catch (error) {
console.log(error);
}
};
// </startMonitoringEvents>
}
index.js
This is the module that you use to run the application and set required constant variables.
import MyApp from './app.js';
import MyAuth from './auth.js';
// Replace the string literal values with your own client id, client secret,
// hub name, project name and component name.
const clientId = '<YOUR_CLIENT_ID>';
const clientSecret = '<YOUR_CLIENT_SECRET>';
const hubName = '<YOUR_HUB_NAME>';
const projectName = '<YOUR_PROJECT_NAME>';
const componentName = '<YOUR_COMPONENT_NAME>';
const eventType = 'MILESTONE_CREATED';
// In a terminal, start ngrok with the following command:
// ngrok http 3000 -host-header="localhost:3000"
// Copy and paste ngrok URL value returned to your terminal console.
const ngrokUrl = '<YOUR_NGROK_URL>';
// Create an instance of auth.js.
let myApsAuth = new MyAuth(clientId, clientSecret);
// Get an access token from your auth.js instance.
let accessToken = await myApsAuth.getAccessToken();
var myApsApp = new MyApp(
accessToken,
ngrokUrl
);
await myApsApp.unsubscribeToEvent(eventType);
if (await myApsApp.subscribeToEvent(hubName, projectName, componentName, eventType)) {
// Use the startMonitoringEvents method to report events to the console.
await myApsApp.startMonitoringEvents();
console.log("\nCreate a milestone in Fusion 360 and wait for the event to be listed here:")
}
auth.js
This is the module that you use to obtain a three-legged access token to be used with the POST request you send to the Manufacturing Data Model API.
It uses the POST /v1/gettoken endpoint.
// Axios is a promise-based HTTP client for the browser and node.js.
// See npmjs.com/package/axios
import axios from 'axios';
// Express is a JavaSscript web application framework. See expressjs.com.
import express from 'express';
// Instantiate an express application.
let app = express();
// Export the Auth class for use by other app modules.
export default class Auth {
// Construct the class instance and set global variables, based on the client ID and secret.
constructor(clientId, clientSecret) {
this.host = 'https://developer.api.autodesk.com/';
this.authAPI = `${this.host}authentication/v1/`;
this.port = 3000;
this.redirectUri = `http://localhost:${this.port}/callback/oauth`;
this.accessTokenPromise = new Promise((resolve, reject) => {
this.resolve = resolve;
this.reject = reject;
});
// Handle the callback/redirection by the Autodesk server once the user approves our app’s access to their data.
app.get('/callback/oauth', async (req, res) => {
const { code } = req.query;
// When you are redirected to the callback URL, the URL also contains a ‘code’ parameter with a value that you can exchange for an actual access token.
try {
const response = await axios({
method: 'POST',
url: `${this.authAPI}gettoken`,
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
data: `client_id=${clientId}&client_secret=${clientSecret}&grant_type=authorization_code&code=${code}&redirect_uri=${this.redirectUri}`
})
// Set the accessToken variable to the value in the response.
this.accessToken = response.data.access_token
// Resolve the Promise passed by the getAccessToken() function below with the access token.
// Let the rest of the application continue.
this.resolve(this.accessToken);
// No need to listen for incoming calls anymore.
this.server.close();
res.redirect('/');
} catch (error) {
console.log(error);
this.reject(error);
}
});
app.get('/', async (req, res) => {
// Once you have the access token, then there is nothing more to do.
if (this.accessToken) {
res.send('Got the access token. You can close the browser!').end();
return;
}
// Otherwise, redirect the user to the Autodesk log-in site where they can log in with their credentials
// and approve our app’s access to their data.
// Once that happens, the Autodesk server redirects the user to the callback URL provided.
// That callback is handled above in the app.get('/callback/oauth' …) function.
const url =
`${this.authAPI}authorize?response_type=code` +
`&client_id=${clientId}` +
`&redirect_uri=${this.redirectUri}` +
'&scope=data:read data:write data:create';
res.redirect(url);
})
this.server = app.listen(this.port);
console.log(
`Open http://localhost:${this.port} in a web browser in order to log in with your Autodesk account!`
);
}
// Pass back a Promise that only resolves and lets the rest of the application continue
// once you have an access token.
getAccessToken = async () => {
return this.accessTokenPromise;
}
}