NotesWhat is notes.io?

Notes brand slogan

Notes - notes.io

Below is a complete, cleaned-up, production-ready version of your controller + service + route setup using only the APS SDK (@aps_sdk/*) (no forge-apis), with version compatibility and best practices. You can drop this into your project (adjust paths) and it should work, assuming your credentials and URNs are correct.

I’ve also added extra logging and checks to help debug if things go wrong.


---

📦 Recommended dependencies (versions)

In your package.json (or install via npm), ensure you have:

"dependencies": {
"@aps_sdk/autodesk-sdkmanager": "^1.0.0",
"@aps_sdk/authentication": "^1.0.0",
"@aps_sdk/oss": "^1.0.0",
"@aps_sdk/model-derivative": "^1.2.0",
"express": "^4.18.0",
"uuid": "^9.0.0",
"dotenv": "^10.0.0",
"axios": "^1.5.0"
}

You can run:

npm install @aps_sdk/autodesk-sdkmanager@latest @aps_sdk/authentication@latest @aps_sdk/oss@latest @aps_sdk/model-derivative@latest express uuid dotenv axios

According to npm, @aps_sdk/model-derivative latest is 1.2.0.
The migration guide from APS states that the SDK is split into modules and you should use AuthenticationClient, ModelDerivativeClient, etc.


---

🗄 Directory structure (suggested)

src/
controllers/
autodesk.controller.js
services/
autodesk.service.js
routes/
autodesk.routes.js
app.js
.env


---

🔐 Environment setup

Create a .env file in your root:

FORGE_CLIENT_ID=your_client_id_here
FORGE_CLIENT_SECRET=your_client_secret_here
PORT=3000

Ensure your process is loading .env (we’ll use dotenv).


---

📄 services/autodesk.service.js

// services/autodesk.service.js
import { SdkManagerBuilder } from "@aps_sdk/autodesk-sdkmanager";
import { AuthenticationClient, Scopes } from "@aps_sdk/authentication";
import { OssClient } from "@aps_sdk/oss";
import { ModelDerivativeClient } from "@aps_sdk/model-derivative";
import dotenv from "dotenv";
import axios from "axios";

dotenv.config();

const { FORGE_CLIENT_ID, FORGE_CLIENT_SECRET } = process.env;
if (!FORGE_CLIENT_ID || !FORGE_CLIENT_SECRET) {
throw new Error("Missing FORGE_CLIENT_ID or FORGE_CLIENT_SECRET in environment variables");
}

// Initialize SDK & clients
const sdkManager = SdkManagerBuilder.create().build();
const authClient = new AuthenticationClient(sdkManager);
const ossClient = new OssClient(sdkManager);
const modelDerivativeClient = new ModelDerivativeClient(sdkManager);

// Helper to get 2-legged token
async function getInternalToken(scopes = [Scopes.DataRead, Scopes.ViewablesRead]) {
const cred = await authClient.getTwoLeggedToken(
FORGE_CLIENT_ID,
FORGE_CLIENT_SECRET,
scopes
);
return cred; // { access_token, expires_in, refresh_token? etc. }
}

// SERVICE methods
const service = {
// Basic token getter
getInternalToken,

// Public token (if you want to expose read-only to clients)
getPublicToken: async () => {
const cred = await authClient.getTwoLeggedToken(
FORGE_CLIENT_ID,
FORGE_CLIENT_SECRET,
[Scopes.DataRead]
);
return cred;
},

// Ensure a bucket exists (for file upload)
ensureBucketExists: async (bucketKey) => {
const { access_token } = await service.getInternalToken();
try {
await ossClient.getBucketDetails(access_token, bucketKey);
} catch (err) {
const status = err?.axiosError?.response?.status;
if (status === 404) {
// bucket not found, create
await ossClient.createBucket(access_token, bucketKey, {
policyKey: "temporary", // or your desired policy
});
} else {
console.error("Error in ensureBucketExists:", err);
throw err;
}
}
},

listBuckets: async () => {
const { access_token } = await service.getInternalToken();
const resp = await ossClient.getBuckets(access_token);
return resp.items;
},

listObjects: async (bucketKey) => {
const { access_token } = await service.getInternalToken();
const resp = await ossClient.getObjects(access_token, bucketKey, { limit: 64 });
return resp.items;
},

uploadObject: async (objectName, filePath, bucketKey) => {
await service.ensureBucketExists(bucketKey);
const { access_token } = await service.getInternalToken();
const obj = await ossClient.upload(bucketKey, objectName, filePath, access_token);
return obj; // contains e.g. objectId, objectKey
},

// Start translation of the uploaded model
translateObject: async (urn, rootFilename) => {
const { access_token } = await service.getInternalToken();
const job = await modelDerivativeClient.startJob(access_token, {
input: {
urn,
compressedUrn: !!rootFilename,
rootFilename,
},
output: {
formats: [
{
views: ["2d", "3d"],
type: "svf",
},
],
},
});
return job.result;
},

// Get manifest of a translated model
getManifest: async (urn) => {
const { access_token } = await service.getInternalToken();
try {
const manifest = await modelDerivativeClient.getManifest(access_token, urn);
return manifest;
} catch (err) {
const status = err?.axiosError?.response?.status;
if (status === 404) {
return null;
}
console.error("Error in getManifest:", err?.axiosError?.response?.data || err.message);
throw err;
}
},

// Check translation status (wrapper around manifest)
checkTranslationStatus: async (urn) => {
const manifest = await service.getManifest(urn);
if (!manifest) {
return { status: "not_translated" };
}
return manifest.result || { status: manifest.status };
},

// MATERIALS EXTRACTION METHODS

getMetadata: async (urn) => {
const { access_token } = await service.getInternalToken([Scopes.DataRead]);
try {
const metadata = await modelDerivativeClient.getMetadata({
urn,
accessToken: access_token,
});
return metadata;
} catch (err) {
console.error("Error fetching metadata:", err?.axiosError?.response?.data || err.message);
throw new Error("Failed to fetch metadata: " + (err.message || ""));
}
},

getRawProperties: async (urn, guid) => {
const { access_token } = await service.getInternalToken([Scopes.DataRead]);
try {
const props = await modelDerivativeClient.getModelProperties({
urn,
guid,
accessToken: access_token,
});
return props.data?.collection || [];
} catch (err) {
console.error("Error fetching properties:", err?.axiosError?.response?.data || err.message);
throw new Error("Failed to fetch raw properties: " + (err.message || ""));
}
},

extractMaterialsFromProperties: async (urn) => {
const metadata = await service.getMetadata(urn);
if (!metadata?.data?.metadata || metadata.data.metadata.length === 0) {
throw new Error("No metadata found. Model may not be translated yet.");
}
const guid = metadata.data.metadata[0].guid;
if (!guid) {
throw new Error("No GUID found in metadata");
}
const collection = await service.getRawProperties(urn, guid);

const materials = [];
for (const item of collection) {
const { properties, name, externalId } = item;
if (!properties) continue;

for (const categoryKey of Object.keys(properties)) {
const props = properties[categoryKey];
if (!props || typeof props !== "object") continue;

const materialName =
props["Material"] ??
props["Material Name"] ??
props["Finish"] ??
props["Render Material"] ??
null;

if (materialName) {
materials.push({
material: materialName,
category: categoryKey,
element: name,
externalId,
});
}
}
}
return materials;
},

// Helper to convert ID to URN (base64)
urnify: (objectId) => {
return Buffer.from(objectId).toString("base64").replace(/=/g, "");
},

// PDF / derivative listing (if needed)
listPdfDerivatives: async (urn) => {
const manifest = await service.getManifest(urn);
if (!manifest) return [];

const results = [];
function walk(node) {
if (!node) return;
const mime = node.mime || node.mimeType || "";
if (mime.toLowerCase().includes("pdf")) {
results.push({
name: node.name || node.urn || "",
derivativeUrn: node.urn || node.guid || "",
mime,
});
}
if (Array.isArray(node.children)) {
node.children.forEach(walk);
}
}
if (Array.isArray(manifest.derivatives)) {
manifest.derivatives.forEach(walk);
} else {
walk(manifest);
}
return results;
},

constructPdfUrl: (derivativeUrn) => {
// endpoint pattern for downloading derivative
return `https://developer.api.autodesk.com/model-derivative/v2/viewers/7.*/output/Resource/${derivativeUrn.split(":")[2]}`;
},

getFileNameFromUrn: (derivativeUrn) => {
const parts = derivativeUrn.split("/");
return parts[parts.length - 1] || "file.pdf";
},
};

export default service;


---

🧭 controllers/autodesk.controller.js

// controllers/autodesk.controller.js
import { v4 as uuidv4 } from "uuid";
import service from "../services/autodesk.service.js";

const JOBS = new Map();

export const GetAutoDeskToken = async (req, res, next) => {
try {
const token = await service.getPublicToken();
return res.json(token);
} catch (err) {
next(err);
}
};

export const UploadFile = async (req, res, next) => {
try {
const file = req.files?.fileToUpload;
if (!file) {
return res.status(400).json({ error: "File is required" });
}
const bucketKey = req.fields?.bucketKey;
if (!bucketKey) {
return res.status(400).json({ error: "bucketKey is required" });
}
const uploaded = await service.uploadObject(file.name, file.path, bucketKey);
const urn = service.urnify(uploaded.objectId);
await service.translateObject(urn, req.fields["model-zip-entrypoint"]);
return res.json({ name: uploaded.objectKey, urn });
} catch (err) {
next(err);
}
};

export const GetAutoDeskModelBuckets = async (req, res, next) => {
try {
const bucketName = req.query.id;
if (!bucketName || bucketName === "#") {
const buckets = await service.listBuckets();
return res.json(
buckets.map((b) => ({
id: b.bucketKey,
text: b.bucketKey,
type: "bucket",
children: true,
}))
);
} else {
const objects = await service.listObjects(bucketName);
return res.json(
objects.map((obj) => ({
id: service.urnify(obj.objectId),
text: obj.objectKey,
type: "object",
children: false,
}))
);
}
} catch (err) {
next(err);
}
};

export const GetAutoDeskManiFest = async (req, res, next) => {
try {
const urn = req.query.urn;
if (!urn) {
return res.status(400).json({ error: "urn required" });
}
const manifest = await service.getManifest(urn);
return res.json(manifest);
} catch (err) {
next(err);
}
};

export const convertRVTorDWGToPDF = async (req, res, next) => {
try {
const urn = req.fields?.urn;
const rootFilename = req.fields?.rootFilename || "root";
if (!urn) {
return res.status(400).json({ error: "urn is required" });
}
const job = await service.translateToPdf(urn, rootFilename);
return res.json({
message: "Translation job started",
job,
urn,
statusCheck: `/convert-to-pdf/translation/status?urn=${encodeURIComponent(urn)}`,
});
} catch (err) {
next(err);
}
};

export const translationPDFStatus = async (req, res, next) => {
try {
const urn = req.query.urn;
if (!urn) {
return res.status(400).json({ error: "urn required" });
}
const manifest = await service.getManifest(urn);
if (!manifest) {
return res.status(404).json({ status: "not_found" });
}
const status = manifest.result?.status || manifest.status || "unknown";
return res.json({ status, manifest });
} catch (err) {
next(err);
}
};

export const listPdfDerivatives = async (req, res, next) => {
try {
const urn = req.query.urn;
if (!urn) {
return res.status(400).json({ error: "urn required" });
}
const pdfs = await service.listPdfDerivatives(urn);
return res.json({ pdfs });
} catch (err) {
next(err);
}
};

export const downloadPdf = async (req, res, next) => {
try {
const derivativeUrn = req.query.derivativeUrn;
if (!derivativeUrn) {
return res.status(400).json({ error: "derivativeUrn is required" });
}
const { access_token } = await service.getInternalToken();
const pdfUrl = service.constructPdfUrl(derivativeUrn);
const pdfResp = await service.axios?.get
? service.axios.get(pdfUrl, {
responseType: "arraybuffer",
headers: { Authorization: `Bearer ${access_token}` },
})
: await (await import("axios")).default({
method: "get",
url: pdfUrl,
responseType: "arraybuffer",
headers: { Authorization: `Bearer ${access_token}` },
});
res.set({
"Content-Type": "application/pdf",
"Content-Disposition": `attachment; filename="${service.getFileNameFromUrn(derivativeUrn)}"`,
"Content-Length": pdfResp.data.length,
});
return res.send(pdfResp.data);
} catch (err) {
console.error("downloadPdf error:", err);
next(err);
}
};

// MATERIALS EXTRACTION

export const startMaterialExtraction = async (req, res, next) => {
try {
const { urn } = req.body;
if (!urn) {
return res.status(400).json({ error: "urn is required" });
}
const jobId = uuidv4();
const job = {
jobId,
urn,
status: "queued",
result: null,
error: null,
createdAt: new Date(),
};
JOBS.set(jobId, job);

(async () => {
try {
job.status = "started";
JOBS.set(jobId, job);
const materials = await service.extractMaterialsFromProperties(urn);
job.status = "completed";
job.result = {
materials,
count: materials.length,
};
} catch (err) {
job.status = "failed";
job.error = err.message || String(err);
console.error("Material extraction job failed:", err);
} finally {
JOBS.set(jobId, job);
}
})();

return res.json({ jobId, status: job.status });
} catch (outerErr) {
console.error("startMaterialExtraction error:", outerErr);
return res.status(500).json({ error: "Internal server error" });
}
};

export const getMaterialJobStatus = (req, res) => {
const jobId = req.query.jobId;
if (!jobId) {
return res.status(400).json({ error: "jobId is required" });
}
const job = JOBS.get(jobId);
if (!job) {
return res.status(404).json({ error: "Job not found" });
}
return res.json(job);
};

export const getMaterialResult = (req, res) => {
const jobId = req.query.jobId;
const job = JOBS.get(jobId);
if (!job) {
return res.status(404).json({ error: "job not found" });
}
if (job.status !== "completed") {
return res.status(202).json(job); // still processing or failed
}
return res.json(job.result);
};

export const getMaterialsLive = async (req, res, next) => {
try {
const urn = req.params.urn;
const mats = await service.extractMaterialsFromProperties(urn);
return res.json({ count: mats.length, materials: mats });
} catch (err) {
next(err);
}
};

export const getModelMetadata = async (req, res, next) => {
try {
const urn = req.params.urn;
const md = await service.getMetadata(urn);
return res.json(md);
} catch (err) {
next(err);
}
};

export const getRawPropertyTree = async (req, res, next) => {
try {
const { urn, guid } = req.params;
const coll = await service.getRawProperties(urn, guid);
return res.json({ count: coll.length, collection: coll });
} catch (err) {
next(err);
}
};


---

🛣 routes/autodesk.routes.js

// routes/autodesk.routes.js
import { Router } from "express";
import ExpressFormidable from "express-formidable";
import * as ctl from "../controllers/autodesk.controller.js";

const router = Router();

router.get("/get-token", ctl.GetAutoDeskToken);

router.post(
"/upload-file",
ExpressFormidable({ maxFileSize: Infinity }),
ctl.UploadFile
);

router.get("/models/buckets", ctl.GetAutoDeskModelBuckets);
router.get("/manifest", ctl.GetAutoDeskManiFest);

router.post(
"/convert-to-pdf",
ExpressFormidable({ maxFileSize: Infinity }),
ctl.convertRVTorDWGToPDF
);
router.get("/convert-to-pdf/translation/status", ctl.translationPDFStatus);
router.get("/translation/pdfs", ctl.listPdfDerivatives);
router.get("/translation/download", ctl.downloadPdf);

router.post("/materials/extract", ctl.startMaterialExtraction);
router.get("/materials/status", ctl.getMaterialJobStatus);
router.get("/materials/result", ctl.getMaterialResult);
router.get("/materials/live/:urn", ctl.getMaterialsLive);
router.get("/materials/metadata/:urn", ctl.getModelMetadata);
router.get("/materials/raw-properties/:urn/:guid", ctl.getRawPropertyTree);

export default router;


---

🏁 app.js (entry point)

// app.js
import express from "express";
import autodDeskRouter from "./routes/autodesk.routes.js";
import dotenv from "dotenv";

dotenv.config();

const app = express();
app.use(express.json());
app.use("/auto-desk", autodDeskRouter);

// Basic error handler
app.use((err, req, res, next) => {
console.error("Unhandled error:", err);
res.status(500).json({ error: err.message || "Internal server error" });
});

const port = process.env.PORT || 3000;
app.listen(port, () => {
console.log(`Server listening on http://localhost:${port}`);
});


---

✅ Summary & things to verify

1. Remove any forge-apis imports (you no longer need them).


2. Use flat method calls: modelDerivativeClient.getMetadata(...) and modelDerivativeClient.getModelProperties(...) (not .metadata.getMetadata)


3. Confirm the installed versions of APS SDK modules.


4. Ensure your URN is correctly base64‐encoded (no urn: prefix).


5. Ensure the model has been translated (manifest available) before extracting metadata.


6. Add debug logs (e.g. console.log(modelDerivativeClient)) if properties/methods are missing.



If after using this full code, you still face errors, share:

The exact error message and stack trace

The output of console.log(Object.keys(modelDerivativeClient))

The version numbers you have installed (npm ls @aps_sdk/model-derivative)


I’ll help you further debug.

     
 
what is notes.io
 

Notes is a web-based application for online taking notes. You can take your notes and share with others people. If you like taking long notes, notes.io is designed for you. To date, over 8,000,000,000+ notes created and continuing...

With notes.io;

  • * You can take a note from anywhere and any device with internet connection.
  • * You can share the notes in social platforms (YouTube, Facebook, Twitter, instagram etc.).
  • * You can quickly share your contents without website, blog and e-mail.
  • * You don't need to create any Account to share a note. As you wish you can use quick, easy and best shortened notes with sms, websites, e-mail, or messaging services (WhatsApp, iMessage, Telegram, Signal).
  • * Notes.io has fabulous infrastructure design for a short link and allows you to share the note as an easy and understandable link.

Fast: Notes.io is built for speed and performance. You can take a notes quickly and browse your archive.

Easy: Notes.io doesn’t require installation. Just write and share note!

Short: Notes.io’s url just 8 character. You’ll get shorten link of your note when you want to share. (Ex: notes.io/q )

Free: Notes.io works for 14 years and has been free since the day it was started.


You immediately create your first note and start sharing with the ones you wish. If you want to contact us, you can use the following communication channels;


Email: [email protected]

Twitter: http://twitter.com/notesio

Instagram: http://instagram.com/notes.io

Facebook: http://facebook.com/notesio



Regards;
Notes.io Team

     
 
Shortened Note Link
 
 
Looding Image
 
     
 
Long File
 
 

For written notes was greater than 18KB Unable to shorten.

To be smaller than 18KB, please organize your notes, or sign in.