├── docs ├── logo.png └── tutorials │ ├── authentication.md │ ├── model-derivative.md │ ├── index.json │ ├── data-management.md │ ├── reality-capture.md │ ├── design-automation.md │ └── webhooks.md ├── .npmignore ├── .gitignore ├── notes.md ├── src ├── index.ts ├── common.ts ├── authentication.ts ├── reality-capture.ts ├── webhooks.ts ├── model-derivative.ts ├── data-management.ts └── design-automation.ts ├── webpack.config.js ├── test ├── authentication.js ├── model-derivative.js ├── webhooks.js ├── bim360.js ├── design-automation.js ├── browser │ └── basic.html ├── reality-capture.js └── data-management.js ├── browser-upload-test.html ├── .github └── workflows │ └── npm-publish.yml ├── package.json ├── tsconfig.json ├── README.md └── CHANGELOG.md /docs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petrbroz/aps-sdk-node/HEAD/docs/logo.png -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .vscode/ 3 | test/ 4 | *.log 5 | .DS_Store 6 | Thumbs.db 7 | .gitignore 8 | .travis.yml 9 | .jsdoc.json -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | .vscode/ 4 | docs/forge-server-utils/ 5 | docs/aps-sdk-node/ 6 | tmp/ 7 | *.log 8 | .DS_Store 9 | Thumbs.db 10 | -------------------------------------------------------------------------------- /notes.md: -------------------------------------------------------------------------------- 1 | - make the size of uploaded chunks configurable 2 | - consider adding a parallelized version of OSS upload 3 | - consider adding a chunked (and parallelized) version of OSS download 4 | - test if the uploads/downloads work with 3-legged OAuth tokens as well 5 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './authentication'; 2 | export * from './data-management'; 3 | export * from './model-derivative'; 4 | export * from './bim360'; 5 | export * from './design-automation'; 6 | export * from './webhooks'; 7 | export * from './reality-capture'; 8 | export { BaseClient } from './common'; 9 | -------------------------------------------------------------------------------- /docs/tutorials/authentication.md: -------------------------------------------------------------------------------- 1 | # Basic usage of Authentication APIs 2 | 3 | ```js 4 | const { AuthenticationClient } = require('aps-sdk-node'); 5 | const { APS_CLIENT_ID, APS_CLIENT_SECRET } = process.env; 6 | const auth = new AuthenticationClient(APS_CLIENT_ID, APS_CLIENT_SECRET); 7 | const authentication = await auth.authenticate(['bucket:read', 'data:read']); 8 | console.log('2-legged Token', authentication.access_token); 9 | ``` 10 | -------------------------------------------------------------------------------- /docs/tutorials/model-derivative.md: -------------------------------------------------------------------------------- 1 | # Basic usage of Model Derivative APIs 2 | 3 | ```js 4 | const { ModelDerivativeClient } = require('aps-sdk-node'); 5 | const { APS_CLIENT_ID, APS_CLIENT_SECRET } = process.env; 6 | const derivatives = new ModelDerivativeClient({ client_id: APS_CLIENT_ID, client_secret: APS_CLIENT_SECRET }); 7 | const job = await derivatives.submitJob('', [{ type: 'svf', views: ['2d', '3d'] }]); 8 | console.log('Job', job); 9 | ``` 10 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const NodePolyfillPlugin = require('node-polyfill-webpack-plugin'); 3 | 4 | module.exports = { 5 | mode: 'production', 6 | entry: './dist/index.js', 7 | output: { 8 | filename: 'aps-sdk-node.js', 9 | path: path.resolve(__dirname, 'dist', 'browser'), 10 | library: 'APS' 11 | }, 12 | plugins: [ 13 | new NodePolyfillPlugin() 14 | ], 15 | devtool: 'source-map' 16 | }; 17 | -------------------------------------------------------------------------------- /docs/tutorials/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "authentication": { 3 | "title": "Authentication API" 4 | }, 5 | "data-management": { 6 | "title": "Data Management API" 7 | }, 8 | "model-derivative": { 9 | "title": "Model Derivative API" 10 | }, 11 | "design-automation": { 12 | "title": "Design Automation API" 13 | }, 14 | "webhooks": { 15 | "title": "Webhooks API" 16 | }, 17 | "reality-capture": { 18 | "title": "Reality Capture API" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /docs/tutorials/data-management.md: -------------------------------------------------------------------------------- 1 | # Basic usage of Data Management APIs 2 | 3 | ```js 4 | const { DataManagementClient } = require('aps-sdk-node'); 5 | const { APS_CLIENT_ID, APS_CLIENT_SECRET } = process.env; 6 | const data = new DataManagementClient({ client_id: APS_CLIENT_ID, client_secret: APS_CLIENT_SECRET }); 7 | 8 | const buckets = await data.listBuckets(); 9 | console.log('Buckets', buckets.map(bucket => bucket.bucketKey).join(',')); 10 | 11 | const objects = await data.listObjects('foo-bucket'); 12 | console.log('Objects', objects.map(object => object.objectId).join(',')); 13 | ``` 14 | -------------------------------------------------------------------------------- /docs/tutorials/reality-capture.md: -------------------------------------------------------------------------------- 1 | # Basic usage of Reality Capture APIs 2 | 3 | ```js 4 | const { OutputFormat, RealityCaptureClient, SceneType } = require('aps-sdk-node'); 5 | const { APS_CLIENT_ID, APS_CLIENT_SECRET } = process.env; 6 | const recap = new RealityCaptureClient({ client_id: APS_CLIENT_ID, client_secret: APS_CLIENT_SECRET }); 7 | const options = { 8 | scenename: '', 9 | scenetype: SceneType.Aerial, 10 | format: OutputFormat.RecapPhotoMesh, 11 | callback: '' 12 | }; 13 | const photoscene = await recap.createPhotoScene(options); 14 | console.log('Photoscene', photoscene); 15 | ``` 16 | -------------------------------------------------------------------------------- /docs/tutorials/design-automation.md: -------------------------------------------------------------------------------- 1 | # Basic usage of Design Automation APIs 2 | 3 | ```js 4 | const { DesignAutomationClient } = require('aps-sdk-node'); 5 | const { APS_CLIENT_ID, APS_CLIENT_SECRET } = process.env; 6 | const client = new DesignAutomationClient({ client_id: APS_CLIENT_ID, client_secret: APS_CLIENT_SECRET }); 7 | const bundles = await client.listAppBundles(); 8 | console.log('App bundles', bundles); 9 | ``` 10 | 11 | ## Creating new app bundle 12 | 13 | ```js 14 | const { DesignAutomationClient } = require('aps-sdk-node'); 15 | const { APS_CLIENT_ID, APS_CLIENT_SECRET } = process.env; 16 | const client = new DesignAutomationClient({ client_id: APS_CLIENT_ID, client_secret: APS_CLIENT_SECRET }); 17 | await client.createAppBundle('', ''); 18 | ``` 19 | -------------------------------------------------------------------------------- /test/authentication.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | 3 | const { AuthenticationClient } = require('..'); 4 | 5 | describe('AuthenticationClient', function() { 6 | beforeEach(function() { 7 | const { APS_CLIENT_ID, APS_CLIENT_SECRET } = process.env; 8 | assert(APS_CLIENT_ID); 9 | assert(APS_CLIENT_SECRET); 10 | this.client = new AuthenticationClient(APS_CLIENT_ID, APS_CLIENT_SECRET); 11 | }); 12 | 13 | describe('authenticate()', function() { 14 | it('should return a token for valid scopes', async function() { 15 | const authentication = await this.client.authenticate(['viewables:read']); 16 | assert(authentication.access_token); 17 | assert(authentication.expires_in > 0); 18 | }); 19 | 20 | it('should reject promise for invalid scopes', function(done) { 21 | this.client.authenticate(['bad:scope']) 22 | .catch((err) => { done(); }); 23 | }); 24 | }); 25 | }); -------------------------------------------------------------------------------- /docs/tutorials/webhooks.md: -------------------------------------------------------------------------------- 1 | # Basic usage of Webhooks APIs 2 | 3 | ```js 4 | const { WebhooksClient, WebhookSystem, WebhookEvent } = require('aps-sdk-node'); 5 | const { APS_CLIENT_ID, APS_CLIENT_SECRET } = process.env; 6 | const webhooks = new WebhooksClient({ client_id: APS_CLIENT_ID, client_secret: APS_CLIENT_SECRET }); 7 | async function run() { 8 | // Enumerate through paginated webhooks 9 | for await (const hooks of webhooks.iterateHooks()) { 10 | console.log(hooks); 11 | } 12 | // Create new webhook for when a folder is deleted 13 | const webhook = await webhooks.createHook(WebhookSystem.Data, WebhookEvent.DataFolderDeleted, { 14 | callbackUrl: 'http://bf067e05.ngrok.io/callback', 15 | scope: { folder: 'urn:adsk.wipprod:fs.folder:co.wT5lCWlXSKeo3razOfHJAw' }, 16 | hookAttribute: { 17 | foo: 33, 18 | myobject: { 19 | nested: true 20 | } 21 | }, 22 | filter: "$[?(@.ext=='txt')]" 23 | }); 24 | console.log('New webhook', webhook); 25 | } 26 | run(); 27 | ``` 28 | -------------------------------------------------------------------------------- /browser-upload-test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | APS Binary Transfer Demo 8 | 9 | 10 | 11 | 12 | 13 | 31 | 32 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages 3 | 4 | name: Publish to NPM 5 | 6 | on: 7 | push: 8 | tags: 9 | - '*' 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: actions/setup-node@v1 17 | with: 18 | node-version: 16 19 | - run: yarn install 20 | - run: yarn run build 21 | - run: yarn run test 22 | env: 23 | APS_BUCKET: ${{secrets.APS_BUCKET}} 24 | APS_CLIENT_ID: ${{secrets.APS_CLIENT_ID}} 25 | APS_CLIENT_SECRET: ${{secrets.APS_CLIENT_SECRET}} 26 | APS_MODEL_URN: ${{secrets.APS_MODEL_URN}} 27 | - run: yarn run docs 28 | 29 | publish: 30 | needs: build 31 | runs-on: ubuntu-latest 32 | steps: 33 | - uses: actions/checkout@v2 34 | - uses: actions/setup-node@v1 35 | with: 36 | node-version: 16 37 | registry-url: https://registry.npmjs.org/ 38 | - run: yarn install 39 | - run: yarn run build 40 | - run: yarn run docs 41 | - run: yarn publish 42 | env: 43 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aps-sdk-node", 3 | "version": "10.0.1", 4 | "description": "Unofficial Autodesk Platform Services SDK for Node.js.", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "build": "yarn run build:node && yarn run build:web", 8 | "build:node": "tsc", 9 | "build:web": "webpack", 10 | "test": "mocha -u bdd", 11 | "docs": "typedoc --out dist/docs src" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/petrbroz/aps-sdk-node.git" 16 | }, 17 | "keywords": [ 18 | "autodesk-platform-services", 19 | "api", 20 | "node.js", 21 | "typescript" 22 | ], 23 | "author": "Petr Broz ", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/petrbroz/aps-sdk-node/issues" 27 | }, 28 | "homepage": "https://github.com/petrbroz/aps-sdk-node#readme", 29 | "devDependencies": { 30 | "@types/node": "^16", 31 | "mocha": "^10.4.0", 32 | "typedoc": "^0.25.13", 33 | "typescript": "^5.1.6", 34 | "webpack": "^5.91.0", 35 | "webpack-cli": "^5.1.4" 36 | }, 37 | "dependencies": { 38 | "@types/adm-zip": "^0.5.5", 39 | "@types/fs-extra": "^11.0.4", 40 | "adm-zip": "^0.5.14", 41 | "axios": "^1.7.2", 42 | "form-data": "^4.0.0", 43 | "fs-extra": "^11.2.0", 44 | "node-polyfill-webpack-plugin": "^4.0.0", 45 | "retry-axios": "^2.6.0" 46 | } 47 | } -------------------------------------------------------------------------------- /test/model-derivative.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | 3 | const { ModelDerivativeClient } = require('..'); 4 | 5 | describe('ModelDerivativeClient', function() { 6 | beforeEach(function() { 7 | const { APS_CLIENT_ID, APS_CLIENT_SECRET, APS_MODEL_URN } = process.env; 8 | assert(APS_CLIENT_ID); 9 | assert(APS_CLIENT_SECRET); 10 | this.client = new ModelDerivativeClient({ client_id: APS_CLIENT_ID, client_secret: APS_CLIENT_SECRET }); 11 | this.urn = APS_MODEL_URN; 12 | }); 13 | 14 | describe('formats()', function() { 15 | it('should return a list of formats', async function() { 16 | const formats = await this.client.formats(); 17 | assert(formats); 18 | }); 19 | }); 20 | 21 | describe('submitJob()', function() { 22 | it('should return a job info', async function() { 23 | const job = await this.client.submitJob(this.urn, [{ type: 'svf', views: ['2d', '3d'] }]); 24 | assert(job); 25 | assert(job.result); 26 | }); 27 | }); 28 | 29 | describe('getManifest()', function() { 30 | it('should return derivative manifest', async function() { 31 | const manifest = await this.client.getManifest(this.urn); 32 | assert(manifest); 33 | }); 34 | }); 35 | 36 | describe('getMetadata()', function() { 37 | it('should return derivative metadata', async function() { 38 | const metadata = await this.client.getMetadata(this.urn); 39 | assert(metadata); 40 | }); 41 | }); 42 | }); -------------------------------------------------------------------------------- /test/webhooks.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | 3 | const { WebhooksClient, WebhookSystem, WebhookEvent } = require('../dist'); 4 | 5 | describe('WebhooksClient', function() { 6 | beforeEach(function() { 7 | const { APS_CLIENT_ID, APS_CLIENT_SECRET } = process.env; 8 | assert(APS_CLIENT_ID); 9 | assert(APS_CLIENT_SECRET); 10 | this.client = new WebhooksClient({ client_id: APS_CLIENT_ID, client_secret: APS_CLIENT_SECRET }); 11 | this.timeout(10000); // Increase timeout to 10 seconds 12 | }); 13 | 14 | describe('listHooks()', function() { 15 | it('should return a list of all webhooks', async function() { 16 | const hooks = await this.client.listHooks(); 17 | assert(hooks); 18 | }); 19 | it('should return a list of webhooks of specific system', async function() { 20 | const hooks = await this.client.listHooks('data'); 21 | assert(hooks); 22 | }); 23 | }); 24 | 25 | describe('iterateHooks()', function() { 26 | it('should iterate over all webhooks', async function() { 27 | for await (const hooks of this.client.iterateHooks()) { 28 | assert(hooks); 29 | break; 30 | } 31 | }); 32 | it('should iterate over webhooks of specific system', async function() { 33 | for await (const hooks of this.client.iterateHooks('data')) { 34 | assert(hooks); 35 | break; 36 | } 37 | }); 38 | }); 39 | 40 | // describe('createHook()', function() { 41 | // it('should create new webhook', async function() { 42 | // const hooks = await this.client.createHook(WebhookSystem.Derivative, undefined, 'http://bf067e05.ngrok.io/callback', { workflow: 'my-workflow-id' }); 43 | // assert(hooks); 44 | // }); 45 | // }); 46 | }); 47 | -------------------------------------------------------------------------------- /test/bim360.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const { BIM360Client } = require('..'); 3 | const { APS_3LEGGED_ACCESS_TOKEN, APS_HUB_ID, APS_PROJECT_ID } = process.env; 4 | 5 | /* 6 | 7 | Due to the 3-legged token requirement, this test is NOT included in the CI/CD pipeline. 8 | In order to run this test manually, run it from command line with the env. variables 9 | listed at the top of this file. 10 | 11 | For example: 12 | 13 | ``` 14 | export APS_3LEGGED_ACCESS_TOKEN= 15 | export APS_HUB_ID= 16 | export APS_PROJECT_ID= 17 | npx mocha test/bim360.js 18 | ``` 19 | 20 | */ 21 | 22 | if (APS_3LEGGED_ACCESS_TOKEN && APS_HUB_ID && APS_PROJECT_ID) { 23 | describe('BIM360Client', function() { 24 | beforeEach(function() { 25 | this.client = new BIM360Client({ token: APS_3LEGGED_ACCESS_TOKEN }); 26 | }); 27 | 28 | describe('listHubs()', function() { 29 | it('should return a list of hubs', async function() { 30 | const hubs = await this.client.listHubs(); 31 | assert(hubs.length > 0); 32 | }); 33 | }); 34 | 35 | describe('getHubDetails()', function() { 36 | it('should return hub details', async function() { 37 | const details = await this.client.getHubDetails(APS_HUB_ID); 38 | assert(details); 39 | }); 40 | }); 41 | 42 | describe('listProjects()', function() { 43 | it('should return a list of projects', async function() { 44 | const projects = await this.client.listProjects(APS_HUB_ID); 45 | assert(projects.length > 0); 46 | }); 47 | }); 48 | 49 | describe('getProjectDetails()', function() { 50 | it('should return a list of projects', async function() { 51 | const details = await this.client.getProjectDetails(APS_HUB_ID, APS_PROJECT_ID); 52 | assert(details); 53 | }); 54 | }); 55 | }); 56 | } 57 | -------------------------------------------------------------------------------- /test/design-automation.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | 3 | const { DesignAutomationClient } = require('..'); 4 | 5 | describe('DesignAutomationClient', function() { 6 | beforeEach(function() { 7 | const { APS_CLIENT_ID, APS_CLIENT_SECRET } = process.env; 8 | assert(APS_CLIENT_ID); 9 | assert(APS_CLIENT_SECRET); 10 | this.client = new DesignAutomationClient({ client_id: APS_CLIENT_ID, client_secret: APS_CLIENT_SECRET }); 11 | this.timeout(10000); // Increase timeout to 10 seconds 12 | }); 13 | 14 | describe('listEngines()', function() { 15 | it('should return a list of engines', async function() { 16 | const engines = await this.client.listEngines(); 17 | assert(engines.length > 0); 18 | }); 19 | }); 20 | 21 | describe('iterateEngines()', function() { 22 | it('should iterate over engines', async function() { 23 | for await (const engines of this.client.iterateEngines(this.bucket)) { 24 | assert(engines.length > 0); 25 | break; 26 | } 27 | }); 28 | }); 29 | 30 | describe('listAppBundles()', function() { 31 | it('should return a list of appbundles', async function() { 32 | const appBundles = await this.client.listAppBundles(); 33 | assert(appBundles.length > 0); 34 | }); 35 | }); 36 | 37 | describe('iterateAppBundles()', function() { 38 | it('should iterate over app bundles', async function() { 39 | for await (const bundles of this.client.iterateAppBundles()) { 40 | assert(bundles.length > 0); 41 | break; 42 | } 43 | }); 44 | }); 45 | 46 | describe('createAppBundle()', function() { 47 | it('should fail because a bundle with this name already exists', function(done) { 48 | this.client.createAppBundle('TestBundle', 'Autodesk.Revit+2019') 49 | .catch((err) => { 50 | //console.log(err); 51 | done(); 52 | }); 53 | }); 54 | }); 55 | 56 | describe('listActivities()', function() { 57 | it('should return a list of activities', async function() { 58 | const activities = await this.client.listActivities(); 59 | assert(activities.length > 0); 60 | }); 61 | }); 62 | 63 | describe('iterateActivities()', function() { 64 | it('should iterate over activities', async function() { 65 | for await (const activities of this.client.iterateActivities()) { 66 | assert(activities.length > 0); 67 | break; 68 | } 69 | }); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /test/browser/basic.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | APS SDK: basic test 7 | 8 | 9 |
10 | 11 | 12 |
13 |
14 | 15 | 16 | 17 |
18 |
19 | 20 | 21 | 22 |
23 | 24 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /test/reality-capture.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | 3 | const { OutputFormat, RealityCaptureClient, SceneType } = require('..'); 4 | 5 | describe('RealityCaptureClient', function() { 6 | beforeEach(function() { 7 | // const { APS_CLIENT_ID, APS_CLIENT_SECRET, SCENE_ID } = process.env; 8 | // assert(APS_CLIENT_ID); 9 | // assert(APS_CLIENT_SECRET); 10 | // assert(SCENE_ID); 11 | // this.client = new RealityCaptureClient({ client_id: APS_CLIENT_ID, client_secret: APS_CLIENT_SECRET}); 12 | // this.sceneid = SCENE_ID; 13 | }); 14 | 15 | describe('createPhotoScene()', function() { 16 | before(function() { 17 | // const { SCENE_NAME, SCENE_CALLBACK, SCENE_FORMAT, SCENE_TYPE} = process.env; 18 | // assert(SCENE_NAME); 19 | // assert(SCENE_CALLBACK); 20 | // assert(SCENE_FORMAT); 21 | // assert(SCENE_TYPE); 22 | // this.scenename = SCENE_NAME; 23 | // this.scenecallback = SCENE_CALLBACK; 24 | // this.sceneformat = SCENE_FORMAT; 25 | // this.scenetype = SCENE_TYPE; 26 | }); 27 | it('should create a new photoscene', async function() { 28 | /* const options = { 29 | scenename: this.scenename, 30 | scenetype: SceneType.Object, 31 | format: OutputFormat.RecapPhotoMesh, 32 | callback: this.callback 33 | }; 34 | const photoscene = await this.client.createPhotoScene(options); 35 | assert(photoscene); */ 36 | }); 37 | }); 38 | 39 | describe('addImages()', function() { 40 | it('should add images to the photoscene', async function() { 41 | /* const images = await this.client.addImages(this.sceneid, this.sceneformat, []); 42 | assert(images); */ 43 | }); 44 | }); 45 | 46 | describe('processPhotoScene()', function() { 47 | it('should start photoscene processing', async function() { 48 | /* const job = await this.client.processPhotoScene(this.sceneid); 49 | assert(job); */ 50 | }); 51 | }); 52 | 53 | describe('getPhotoSceneProgress()', function() { 54 | it('should return processing progress', async function() { 55 | /* const progress = await this.client.getPhotoSceneProgress(this.sceneid); 56 | assert(progress); */ 57 | }); 58 | }); 59 | 60 | describe('getPhotoScene()', function() { 61 | it('should return time-limited HTTPS link to an output file', async function() { 62 | /* const photoscene = await this.client.getPhotoScene(this.sceneid, 'rcm'); 63 | assert(photoscene); */ 64 | }); 65 | }); 66 | 67 | describe('cancelPhotoScene', function() { 68 | it('should abort the processing of a photoscene', async function() { 69 | /* const photoscene = await this.client.cancelPhotoScene(this.sceneid); 70 | assert(photoscene); */ 71 | }); 72 | }); 73 | 74 | describe('deletePhotoScene', function() { 75 | it('should delete a photoscene and its associated assets', async function() { 76 | /* const photoscene = await this.client.deletePhotoScene(this.sceneid); 77 | assert(photoscene); */ 78 | }); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "es2018", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ 5 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 6 | // "lib": [], /* Specify library files to be included in the compilation. */ 7 | // "allowJs": true, /* Allow javascript files to be compiled. */ 8 | // "checkJs": true, /* Report errors in .js files. */ 9 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 10 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 11 | "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 12 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 13 | // "outFile": "./", /* Concatenate and emit output to single file. */ 14 | "outDir": "./dist", /* Redirect output structure to the directory. */ 15 | "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 16 | // "composite": true, /* Enable project compilation */ 17 | // "removeComments": true, /* Do not emit comments to output. */ 18 | // "noEmit": true, /* Do not emit outputs. */ 19 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 20 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 21 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 22 | 23 | /* Strict Type-Checking Options */ 24 | "strict": true, /* Enable all strict type-checking options. */ 25 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 26 | // "strictNullChecks": true, /* Enable strict null checks. */ 27 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 28 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 29 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 30 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 31 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 32 | 33 | /* Additional Checks */ 34 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 35 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 36 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 37 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 38 | 39 | /* Module Resolution Options */ 40 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 41 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 42 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 43 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 44 | // "typeRoots": [], /* List of folders to include type definitions from. */ 45 | // "types": [], /* Type declaration files to be included in compilation. */ 46 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 47 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 48 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 49 | 50 | /* Source Map Options */ 51 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 52 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 53 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 54 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 55 | 56 | /* Experimental Options */ 57 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 58 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # aps-sdk-node 2 | 3 | ![Publish to NPM](https://github.com/petrbroz/aps-sdk-node/workflows/Publish%20to%20NPM/badge.svg) 4 | [![npm version](https://badge.fury.io/js/aps-sdk-node.svg)](https://badge.fury.io/js/aps-sdk-node) 5 | ![node](https://img.shields.io/node/v/aps-sdk-node.svg) 6 | ![npm downloads](https://img.shields.io/npm/dw/aps-sdk-node.svg) 7 | ![platforms](https://img.shields.io/badge/platform-windows%20%7C%20osx%20%7C%20linux-lightgray.svg) 8 | [![license](https://img.shields.io/badge/license-MIT-blue.svg)](http://opensource.org/licenses/MIT) 9 | 10 | Unofficial SDK for accessing [Autodesk Platform Services](https://aps.autodesk.com/) from Node.js applications 11 | and from browsers, built using [TypeScript](https://www.typescriptlang.org) and modern language features like 12 | [async/await](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function) 13 | or [generators](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*). 14 | 15 | ![Autodesk Platform Services](docs/logo.png) 16 | 17 | ## Usage 18 | 19 | ### Server Side 20 | 21 | The TypeScript implementation is transpiled into CommonJS JavaScript module with type definition files, 22 | so you can use it both in Node.js projects, and in TypeScript projects: 23 | 24 | ```js 25 | // JavaScript 26 | const { DataManagementClient } = require('aps-sdk-node'); 27 | ``` 28 | 29 | ```ts 30 | // TypeScript 31 | import { 32 | DataManagementClient, 33 | IBucket, 34 | IObject, 35 | IResumableUploadRange, 36 | DataRetentionPolicy 37 | } from 'aps-sdk-node'; 38 | ``` 39 | 40 | #### Authentication 41 | 42 | If you need to generate [2-legged tokens](https://aps.autodesk.com/en/docs/oauth/v2/tutorials/get-2-legged-token) 43 | manually, you can use the `AuthenticationClient` class: 44 | 45 | ```js 46 | const { AuthenticationClient } = require('aps-sdk-node'); 47 | const { APS_CLIENT_ID, APS_CLIENT_SECRET } = process.env; 48 | const auth = new AuthenticationClient(APS_CLIENT_ID, APS_CLIENT_SECRET); 49 | const authentication = await auth.authenticate(['bucket:read', 'data:read']); 50 | console.log('2-legged token', authentication.access_token); 51 | ``` 52 | 53 | Other API clients in this library are typically configured using a simple JavaScript object 54 | containing either `client_id` and `client_secret` properties (for 2-legged authentication), 55 | or a single `token` property (for authentication using a pre-generated access token): 56 | 57 | ```js 58 | const { DataManagementClient, BIM360Client } = require('aps-sdk-node'); 59 | const dm = new DataManagementClient({ client_id: '...', client_secret: '...' }); 60 | const bim360 = new BIM360Client({ token: '...' }); 61 | ``` 62 | 63 | #### Data Management 64 | 65 | ```js 66 | const { DataManagementClient } = require('aps-sdk-node'); 67 | const { APS_CLIENT_ID, APS_CLIENT_SECRET } = process.env; 68 | const data = new DataManagementClient({ client_id: APS_CLIENT_ID, client_secret: APS_CLIENT_SECRET }); 69 | 70 | const buckets = await data.listBuckets(); 71 | console.log('Buckets', buckets.map(bucket => bucket.bucketKey).join(',')); 72 | 73 | const objects = await data.listObjects('foo-bucket'); 74 | console.log('Objects', objects.map(object => object.objectId).join(',')); 75 | ``` 76 | 77 | #### Model Derivatives 78 | 79 | ```js 80 | const { ModelDerivativeClient } = require('aps-sdk-node'); 81 | const { APS_CLIENT_ID, APS_CLIENT_SECRET } = process.env; 82 | const derivatives = new ModelDerivativeClient({ client_id: APS_CLIENT_ID, client_secret: APS_CLIENT_SECRET }); 83 | const job = await derivatives.submitJob('', [{ type: 'svf', views: ['2d', '3d'] }]); 84 | console.log('Job', job); 85 | ``` 86 | 87 | #### Design Automation 88 | 89 | ```js 90 | const { DesignAutomationClient } = require('aps-sdk-node'); 91 | const { APS_CLIENT_ID, APS_CLIENT_SECRET } = process.env; 92 | const client = new DesignAutomationClient({ client_id: APS_CLIENT_ID, client_secret: APS_CLIENT_SECRET }); 93 | const bundles = await client.listAppBundles(); 94 | console.log('App bundles', bundles); 95 | ``` 96 | 97 | #### Reality Capture 98 | 99 | ```js 100 | const { OutputFormat, RealityCaptureClient, SceneType } = require('aps-sdk-node'); 101 | const { APS_CLIENT_ID, APS_CLIENT_SECRET } = process.env; 102 | const recap = new RealityCaptureClient({ client_id: APS_CLIENT_ID, client_secret: APS_CLIENT_SECRET }); 103 | const options = { 104 | scenename: '', 105 | scenetype: SceneType.Aerial, 106 | format: OutputFormat.RecapPhotoMesh, 107 | callback: '' 108 | }; 109 | const photoscene = await recap.createPhotoScene(options); 110 | console.log('Photoscene', photoscene); 111 | ``` 112 | 113 | ### Client Side (experimental) 114 | 115 | The transpiled output from TypeScript is also bundled using [webpack](https://webpack.js.org), 116 | so you can use the same functionality in a browser. There is a caveat, unfortunately: at the moment 117 | it is not possible to request APS access tokens from the browser 118 | due to [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) limitations, 119 | so when creating instances of the various clients, instead of providing client ID and secret 120 | you will have to provide the token directly. 121 | 122 | ```html 123 | 124 | 134 | ``` 135 | 136 | Note that you can also request a specific version of the library from CDN by appending `@` 137 | to the npm package name, for example, `https://cdn.jsdelivr.net/npm/aps-sdk-node@4.0.0/dist/browser/aps-sdk-node.js`. 138 | 139 | ## Testing 140 | 141 | ```bash 142 | export APS_CLIENT_ID= 143 | export APS_CLIENT_SECRET= 144 | export APS_BUCKET= 145 | export APS_MODEL_URN= 146 | yarn run build # Transpile TypeScript into JavaScript 147 | yarn test 148 | ``` 149 | -------------------------------------------------------------------------------- /src/common.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosRequestConfig, AxiosInstance } from 'axios'; 2 | import * as rax from 'retry-axios'; 3 | 4 | import { AuthenticationClient } from './authentication'; 5 | 6 | const MaxContentLength = Number.MAX_SAFE_INTEGER; 7 | const MaxBodyLength = Number.MAX_SAFE_INTEGER; 8 | 9 | export function sleep(ms: number) { return new Promise(function(resolve) { setTimeout(resolve, ms); }); } 10 | 11 | export const DefaultHost = 'https://developer.api.autodesk.com'; 12 | 13 | export enum Region { 14 | US = 'US', 15 | EMEA = 'EMEA' 16 | } 17 | 18 | export type IAuthOptions = { client_id: string; client_secret: string; } | { token: string; }; 19 | 20 | export type IRequestData = { urlencoded: any; } | { json: any; } | { buffer: any; }; 21 | 22 | export abstract class BaseClient { 23 | protected auth?: AuthenticationClient; 24 | protected token?: string; 25 | protected root: string; 26 | protected host: string; 27 | protected region: Region; 28 | protected axios: AxiosInstance; 29 | 30 | /** 31 | * Initializes new client with specific authentication method. 32 | * @param {string} root Root path for all endpoints (must not contain any slashes at the beginning nor end). 33 | * @param {IAuthOptions} auth Authentication object, 34 | * containing either `client_id` and `client_secret` properties (for 2-legged authentication), 35 | * or a single `token` property (for 2-legged or 3-legged authentication with pre-generated access token). 36 | * @param {string} [host="https://developer.api.autodesk.com"] APS host (must not contain slash at the end). 37 | * @param {Region} [region="US"] APS availability region ("US" or "EMEA"). 38 | */ 39 | constructor(root: string, auth: IAuthOptions, host?: string, region?: Region) { 40 | if ('client_id' in auth && 'client_secret' in auth) { 41 | this.auth = new AuthenticationClient(auth.client_id, auth.client_secret, host); 42 | } else if ('token' in auth) { 43 | this.token = auth.token; 44 | } else { 45 | throw new Error('Authentication parameters missing or incorrect.'); 46 | } 47 | this.root = root; 48 | this.host = host || DefaultHost; 49 | this.region = region || Region.US; 50 | this.axios = axios.create({ 51 | baseURL: this.host + '/' + this.root + '/', 52 | maxContentLength: MaxContentLength, 53 | maxBodyLength: MaxBodyLength 54 | }); 55 | // Attach an interceptor to the axios instance that will retry response codes 100-199, 429, and 500-599. 56 | // For default settings, see https://github.com/JustinBeckwith/retry-axios#usage. 57 | this.axios.defaults.raxConfig = { 58 | instance: this.axios 59 | }; 60 | rax.attach(this.axios); 61 | } 62 | 63 | /** 64 | * Resets client to specific authentication method, APS host, and availability region. 65 | * @param {IAuthOptions} [auth] Authentication object, 66 | * containing either `client_id` and `client_secret` properties (for 2-legged authentication), 67 | * or a single `token` property (for 2-legged or 3-legged authentication with pre-generated access token). 68 | * @param {string} [host="https://developer.api.autodesk.com"] APS host. 69 | * @param {Region} [region="US"] APS availability region ("US" or "EMEA"). 70 | */ 71 | public reset(auth?: IAuthOptions, host?: string, region?: Region) { 72 | if (typeof auth !== 'undefined') { 73 | if ('client_id' in auth && 'client_secret' in auth) { 74 | this.auth = new AuthenticationClient(auth.client_id, auth.client_secret, host); 75 | } else if ('token' in auth) { 76 | this.token = auth.token; 77 | } else { 78 | throw new Error('Authentication parameters missing or incorrect.'); 79 | } 80 | } 81 | if (typeof host !== 'undefined') { 82 | this.host = host || DefaultHost; 83 | } 84 | if (typeof region !== 'undefined') { 85 | this.region = region || Region.US; 86 | } 87 | this.axios = axios.create({ baseURL: this.host + '/' + this.root + '/' }); 88 | } 89 | 90 | protected async setAuthorization(options: any, scopes: string[]) { 91 | options.headers = options.headers || {}; 92 | if (this.auth) { 93 | const authentication = await this.auth.authenticate(scopes); 94 | options.headers['Authorization'] = 'Bearer ' + authentication.access_token; 95 | } else { 96 | options.headers['Authorization'] = 'Bearer ' + this.token; 97 | } 98 | } 99 | 100 | // Makes a general request and returns the entire response (not just its parsed body) 101 | protected async fetch(config: AxiosRequestConfig) { 102 | return this.axios.request(config); 103 | } 104 | 105 | // Helper method for GET requests, 106 | // returning parsed response body or throwing an excetion in case of an issue 107 | protected async get(endpoint: string, headers: { [name: string]: string } = {}, scopes: string[]): Promise { 108 | const config: AxiosRequestConfig = { headers }; 109 | await this.setAuthorization(config, scopes); 110 | const resp = await this.axios.get(endpoint, config); 111 | return resp.data; 112 | } 113 | 114 | // Helper method for GET requests returning binary data, 115 | // throwing an excetion in case of an issue 116 | protected async getBuffer(endpoint: string, headers: { [name: string]: string } = {}, scopes: string[]): Promise { 117 | const config: AxiosRequestConfig = { headers, responseType: 'arraybuffer' }; 118 | await this.setAuthorization(config, scopes); 119 | const resp = await this.axios.get(endpoint, config); 120 | return resp.data; 121 | } 122 | 123 | // Helper method for GET requests returning stream data, 124 | // throwing an excetion in case of an issue 125 | protected async getStream(endpoint: string, headers: { [name: string]: string } = {}, scopes: string[]): Promise { 126 | const config: AxiosRequestConfig = { headers, responseType: 'stream' }; 127 | await this.setAuthorization(config, scopes); 128 | const resp = await this.axios.get(endpoint, config); 129 | return resp.data; 130 | } 131 | 132 | // Helper method for POST requests, 133 | // returning parsed response body of throwing an excetion in case of an issue 134 | protected async post(endpoint: string, data: any, headers: { [name: string]: string } = {}, scopes: string[]): Promise { 135 | const config: AxiosRequestConfig = { headers }; 136 | await this.setAuthorization(config, scopes); 137 | const resp = await this.axios.post(endpoint, data, config); 138 | return resp.data; 139 | } 140 | 141 | // Helper method for PUT requests, 142 | // returning parsed response body of throwing an excetion in case of an issue 143 | protected async put(endpoint: string, data: any, headers: { [name: string]: string } = {}, scopes: string[]): Promise { 144 | const config: AxiosRequestConfig = { headers }; 145 | await this.setAuthorization(config, scopes); 146 | const resp = await this.axios.put(endpoint, data, config); 147 | return resp.data; 148 | } 149 | 150 | // Helper method for PATCH requests, 151 | // returning parsed response body of throwing an excetion in case of an issue 152 | protected async patch(endpoint: string, data: any, headers: { [name: string]: string } = {}, scopes: string[]): Promise { 153 | const config: AxiosRequestConfig = { headers }; 154 | await this.setAuthorization(config, scopes); 155 | const resp = await this.axios.patch(endpoint, data, config); 156 | return resp.data; 157 | } 158 | 159 | // Helper method for DELETE requests, 160 | // returning parsed response body of throwing an excetion in case of an issue 161 | protected async delete(endpoint: string, headers: { [name: string]: string } = {}, scopes: string[]): Promise { 162 | const config: AxiosRequestConfig = { headers }; 163 | await this.setAuthorization(config, scopes); 164 | const resp = await this.axios.delete(endpoint, config); 165 | return resp.data; 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /test/data-management.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const axios = require('axios'); 3 | const { Readable, Writable } = require('stream'); 4 | 5 | const { DataManagementClient } = require('..'); 6 | 7 | describe('DataManagementClient', function() { 8 | beforeEach(function() { 9 | const { APS_CLIENT_ID, APS_CLIENT_SECRET, APS_BUCKET } = process.env; 10 | assert(APS_CLIENT_ID); 11 | assert(APS_CLIENT_SECRET); 12 | assert(APS_BUCKET); 13 | this.client = new DataManagementClient({ client_id: APS_CLIENT_ID, client_secret: APS_CLIENT_SECRET }); 14 | this.bucket = APS_BUCKET; 15 | this.timeout(10000); // Increase timeout to 10 seconds 16 | }); 17 | 18 | describe('listBuckets()', function() { 19 | it('should return a list of buckets', async function() { 20 | const buckets = await this.client.listBuckets(); 21 | assert(buckets.length > 0); 22 | }); 23 | }); 24 | 25 | describe('iterateBuckets()', function() { 26 | it('should iterate over buckets', async function() { 27 | for await (const buckets of this.client.iterateBuckets()) { 28 | assert(buckets.length > 0); 29 | break; 30 | } 31 | }); 32 | }); 33 | 34 | describe('getBucketDetails()', function() { 35 | it('should return bucket info', async function() { 36 | const details = await this.client.getBucketDetails(this.bucket); 37 | assert(details); 38 | assert(details.bucketKey === this.bucket); 39 | assert(details.bucketOwner); 40 | assert(details.createdDate); 41 | assert(details.permissions); 42 | assert(details.policyKey); 43 | }); 44 | }); 45 | 46 | describe('createBucket()', function() { 47 | /* Don't want to create a new bucket on every test run... */ 48 | // it('should create a new bucket', async function() { 49 | // const bucket = await this.client.createBucket('test.123456', 'transient'); 50 | // assert(bucket); 51 | // console.log(bucket); 52 | // }); 53 | 54 | it('should fail because a bucket with this name already exists', function(done) { 55 | this.client.createBucket(this.bucket, 'transient') 56 | .catch((err) => { 57 | //console.log(err); 58 | done(); 59 | }); 60 | }); 61 | }); 62 | 63 | describe('listObjects()', function() { 64 | it('should return a list of objects', async function() { 65 | this.timeout(30000); 66 | const objects = await this.client.listObjects(this.bucket); 67 | assert(objects.length > 0); 68 | }); 69 | it('should only list objects with given prefix', async function() { 70 | this.timeout(30000); 71 | const prefix = 'p'; 72 | const objects = await this.client.listObjects(this.bucket, prefix); 73 | for (const obj of objects) { 74 | assert(obj.objectKey.startsWith(prefix)); 75 | } 76 | }); 77 | }); 78 | 79 | describe('iterateObjects()', function() { 80 | it('should iterate over objects', async function() { 81 | this.timeout(30000); 82 | for await (const objects of this.client.iterateObjects(this.bucket)) { 83 | assert(objects.length > 0); 84 | break; 85 | } 86 | }); 87 | it('should only iterate over objects with given prefix', async function() { 88 | this.timeout(30000); 89 | const prefix = 'p'; 90 | for await (const objects of this.client.iterateObjects(this.bucket, 16, prefix)) { 91 | for (const obj of objects) { 92 | assert(obj.objectKey.startsWith(prefix)); 93 | } 94 | } 95 | }); 96 | }); 97 | 98 | describe('uploadObject()', function() { 99 | it('should upload object content', async function() { 100 | this.timeout(30000); 101 | const objectName = 'test-file'; 102 | const buff = Buffer.from('This is a test string!', 'utf8'); 103 | const result = await this.client.uploadObject(this.bucket, objectName, buff, { contentType: 'text/plain; charset=UTF-8' }); 104 | assert(result); 105 | assert(result.location); 106 | assert(result.location.indexOf(objectName) !== -1); 107 | }); 108 | it('should upload object stream', async function() { 109 | this.timeout(30000); 110 | const objectName = 'test-file-stream'; 111 | const stream = new Readable({ 112 | read() { 113 | this.push(Buffer.from('This is a test string!', 'utf8')); 114 | this.push(null); 115 | } 116 | }); 117 | const result = await this.client.uploadObjectStream(this.bucket, objectName, stream, { contentType: 'text/plain; charset=UTF-8' }); 118 | assert(result); 119 | assert(result.location); 120 | assert(result.location.indexOf(objectName) !== -1); 121 | }); 122 | }); 123 | 124 | describe('downloadObject()', function() { 125 | it('should download object content', async function() { 126 | this.timeout(30000); 127 | const objectName = 'test-file'; 128 | const content = await this.client.downloadObject(this.bucket, objectName); 129 | assert(content.indexOf('This is a test string!') === 0); 130 | }); 131 | it('should download object stream', async function() { 132 | this.timeout(30000); 133 | const objectName = 'test-file-stream'; 134 | const output = new Writable({ 135 | write(chunk) { 136 | if (!this.buff) { 137 | this.buff = chunk; 138 | } else { 139 | this.buff = Buffer.concat(this.buff, chunk); 140 | } 141 | } 142 | }); 143 | const stream = await this.client.downloadObjectStream(this.bucket, objectName); 144 | stream.pipe(output); 145 | stream.on('end', () => { 146 | assert(output.buff.indexOf('This is a test string!') === 0); 147 | }); 148 | }); 149 | }); 150 | 151 | describe('getObjectDetails()', function() { 152 | it('should return object info', async function() { 153 | const details = await this.client.getObjectDetails(this.bucket, 'test-file'); 154 | assert(details.bucketKey === this.bucket); 155 | assert(details.objectId); 156 | assert(details.objectKey === 'test-file'); 157 | }); 158 | }); 159 | 160 | describe('createSignedUrl()', function() { 161 | it('should return signed URL resource info', async function() { 162 | const info = await this.client.createSignedUrl(this.bucket, 'nonexistent-file'); 163 | assert(info); 164 | assert(info.signedUrl); 165 | }); 166 | }); 167 | 168 | describe('uploadObjectResumable()', function() { 169 | it('should upload object in multiple chunks', async function() { 170 | this.timeout(30000); 171 | const arr1 = new Uint8Array(5 << 20); 172 | for (let i = 0; i < arr1.length; i++) { 173 | arr1[i] = i % 255; 174 | } 175 | const arr2 = new Uint8Array(1 << 20); 176 | for (let i = 0; i < arr2.length; i++) { 177 | arr2[i] = i % 255; 178 | } 179 | const objectKey = 'aps-sdk-node-test-file-' + new Date().toISOString(); 180 | const uploadParams = await this.client.getUploadUrls(this.bucket, objectKey, 2, 1); 181 | await axios.put(uploadParams.urls[0], arr1); 182 | await axios.put(uploadParams.urls[1], arr2); 183 | const obj = await this.client.completeUpload(this.bucket, objectKey, uploadParams.uploadKey); 184 | assert(obj.size, arr1.byteLength + arr2.byteLength); 185 | }); 186 | }); 187 | 188 | // describe('copyObject()', function() { 189 | // it('should copy object to another object with new name', async function () { 190 | // const obj = await this.client.copyObject(this.bucket, 'buffer-upload-test.txt', 'buffer-upload-test-copy.txt'); 191 | // assert(obj.objectKey === 'buffer-upload-test-copy.txt'); 192 | // }); 193 | // }); 194 | }); 195 | -------------------------------------------------------------------------------- /src/authentication.ts: -------------------------------------------------------------------------------- 1 | import * as querystring from 'querystring'; 2 | import axios, { AxiosRequestConfig } from 'axios'; 3 | 4 | const RootPath = `authentication/v2`; 5 | 6 | interface ITokenCache { 7 | promise: Promise; 8 | expires_at: number; 9 | } 10 | 11 | export interface ITwoLeggedToken { 12 | access_token: string; 13 | expires_in: number; 14 | } 15 | 16 | export interface IThreeLeggedToken extends ITwoLeggedToken { 17 | refresh_token: string; 18 | } 19 | 20 | export interface IUserProfile { 21 | sub: string; // Oxygen id of the user 22 | name: string; // Full name of the user 23 | given_name: string; // First name of the user 24 | family_name: string; // Last name of the user 25 | preferred_username: string; // Username of the user 26 | email: string; // Primary email of the user 27 | email_verified: boolean; // Flag that shows if the user's email is verified or not 28 | profile: string; // URL for the profile of the user 29 | picture: string; // Profile image of the user (x120 thumbnail) 30 | locale: string; // End-User's locale, represented as a BCP47 standard (eg, en-US) 31 | updated_at: number; // The second-precision Unix timestamp of last modification on the user profile 32 | is_2fa_enabled: boolean; // Flag is true if two factor authentication is enabled for this profile. 33 | country_code: string; // The country code assigned to the account at creation. 34 | address: object; // object containing contact address information 35 | phone_number: string; // The primary phone number of the user profile with country code and extension in the format: "+(countryCode) (phoneNumber) #(Extension)" 36 | phone_number_verified: boolean; // Flag to tell whether or not above phone number was verified 37 | ldap_enabled: boolean; // Flag for the LDAP/SSO Status of the user, true if is ldap user. 38 | ldap_domain: string; // Domain name for the LDAP user null if non LDAP user 39 | job_title: string; // The job title selected on the user profile. 40 | industry: string; // The industry selected on the user profile. 41 | industry_code: string; // The industry code associated on the user profile 42 | about_me: string; // The about me text on the user profile 43 | language: string; // The language selected by the user 44 | company: string; // The company on the user profile 45 | created_date: string; // The datetime (UTC) the user was created 46 | last_login_date: string; // The last login date (UTC) of the user. 47 | eidm_guid: string; // Eidm Identifier.` 48 | opt_in: boolean; // The flag that indicates if user opts in the marketing information. 49 | social_userinfo_list: object[]; // Social provider name and provider identifier when the user is a social user or else empty list. 50 | thumbnails: object; // Object with profile image thumbnail urls for each size by key. 51 | } 52 | 53 | /** 54 | * Client providing access to APS Authentication API ({@link https://aps.autodesk.com/en/docs/oauth/v2/reference/http}). 55 | * @tutorial authentication 56 | */ 57 | export class AuthenticationClient { 58 | private client_id: string; 59 | private client_secret: string; 60 | private host: string; 61 | private _cached: { [key: string]: ITokenCache }; 62 | 63 | get clientId() { return this.client_id; } 64 | 65 | /** 66 | * Initializes new client with specific APS app credentials. 67 | * @param {string} client_id APS application client ID. 68 | * @param {string} client_secret APS application client secret. 69 | * @param {string} [host="https://developer.api.autodesk.com"] APS host. 70 | */ 71 | constructor(client_id: string, client_secret: string, host: string = 'https://developer.api.autodesk.com') { 72 | this.client_id = client_id; 73 | this.client_secret = client_secret; 74 | this.host = host; 75 | this._cached = {}; 76 | } 77 | 78 | // Helper method for POST requests with urlencoded params 79 | protected async post(endpoint: string, params: any, config?: AxiosRequestConfig) { 80 | return axios.post(this.host + '/' + RootPath + '/' + endpoint, querystring.stringify(params), config); 81 | } 82 | 83 | /** 84 | * Retrieves 2-legged access token for a specific set of scopes 85 | * ({@link https://aps.autodesk.com/en/docs/oauth/v2/reference/http/gettoken-POST/}). 86 | * Unless the {@see force} parameter is used, the access tokens are cached 87 | * based on their scopes and the 'expires_in' field in the response. 88 | * @param {string[]} scopes List of requested {@link https://aps.autodesk.com/en/docs/oauth/v2/developers_guide/scopes|scopes}. 89 | * @param {boolean} [force] Skip cache, if there is any, and retrieve a new token. 90 | * @returns {Promise} Promise of 2-legged authentication object containing two fields, 91 | * 'access_token' with the actual token, and 'expires_in' with expiration time (in seconds). 92 | */ 93 | authenticate(scopes: string[], force: boolean = false): Promise { 94 | // Check if there's a cached token, unexpired, and with the same scopes 95 | const key = 'two-legged/' + scopes.join('/'); 96 | if (!force && key in this._cached) { 97 | const cache = this._cached[key]; 98 | if (cache.expires_at > Date.now()) { 99 | return cache.promise.then((token) => ({ 100 | access_token: token, 101 | expires_in: Math.floor((cache.expires_at - Date.now()) / 1000) 102 | })); 103 | } 104 | } 105 | 106 | const params = { 107 | 'grant_type': 'client_credentials', 108 | 'scope': scopes.join(' ') 109 | }; 110 | const headers = { 111 | 'Authorization': `Basic ${Buffer.from(`${this.client_id}:${this.client_secret}`).toString('base64')}` 112 | }; 113 | const cache = this._cached[key] = { 114 | expires_at: Number.MAX_VALUE, 115 | promise: this.post('token', params, { headers }).then((resp) => { 116 | const { data } = resp; 117 | this._cached[key].expires_at = Date.now() + data.expires_in * 1000; 118 | return data.access_token; 119 | }) 120 | }; 121 | return cache.promise.then((token) => ({ 122 | access_token: token, 123 | expires_in: Math.floor((cache.expires_at - Date.now()) / 1000) 124 | })); 125 | } 126 | 127 | /** 128 | * Generates a URL for 3-legged authentication 129 | * ({@link https://aps.autodesk.com/en/docs/oauth/v2/reference/http/authorize-GET/}). 130 | * @param {string[]} scopes List of requested {@link https://aps.autodesk.com/en/docs/oauth/v2/developers_guide/scopes/}. 131 | * @param {string} redirectUri Same redirect URI as defined by the APS app. 132 | * @returns {string} Autodesk login URL. 133 | */ 134 | getAuthorizeRedirect(scopes: string[], redirectUri: string): string { 135 | return `${this.host}/${RootPath}/authorize?response_type=code&client_id=${this.client_id}&redirect_uri=${redirectUri}&scope=${scopes.join(' ')}`; 136 | } 137 | 138 | /** 139 | * Exchanges 3-legged authentication code for an access token 140 | * ({@link https://aps.autodesk.com/en/docs/oauth/v2/reference/http/gettoken-POST/}). 141 | * @async 142 | * @param {string} code Authentication code returned from the Autodesk login process. 143 | * @param {string} redirectUri Same redirect URI as defined by the APS app. 144 | * @returns {Promise} Promise of 3-legged authentication object containing 145 | * 'access_token', 'refresh_token', and 'expires_in' with expiration time (in seconds). 146 | */ 147 | async getToken(code: string, redirectUri: string): Promise { 148 | const params = { 149 | 'grant_type': 'authorization_code', 150 | 'code': code, 151 | 'redirect_uri': redirectUri 152 | }; 153 | const headers = { 154 | 'Authorization': `Basic ${Buffer.from(`${this.client_id}:${this.client_secret}`).toString('base64')}` 155 | }; 156 | const resp = await this.post(`token`, params, { headers }); 157 | return resp.data; 158 | } 159 | 160 | /** 161 | * Refreshes 3-legged access token 162 | * ({@link https://aps.autodesk.com/en/docs/oauth/v2/reference/http/gettoken-POST/}). 163 | * @async 164 | * @param {string[]} scopes List of requested {@link https://aps.autodesk.com/en/docs/oauth/v2/developers_guide/scopes|scopes}. 165 | * @param {string} refreshToken Refresh token. 166 | * @returns {Promise} Promise of 3-legged authentication object containing 167 | * 'access_token', 'refresh_token', and 'expires_in' with expiration time (in seconds). 168 | */ 169 | async refreshToken(scopes: string[], refreshToken: string): Promise { 170 | const params = { 171 | 'grant_type': 'refresh_token', 172 | 'refresh_token': refreshToken, 173 | 'scope': scopes.join(' ') 174 | }; 175 | const headers = { 176 | 'Authorization': `Basic ${Buffer.from(`${this.client_id}:${this.client_secret}`).toString('base64')}` 177 | }; 178 | const resp = await this.post(`token`, params, { headers }); 179 | return resp.data; 180 | } 181 | 182 | /** 183 | * Gets profile information for a user based on their 3-legged auth token 184 | * ({@link https://aps.autodesk.com/en/docs/profile/v1/reference/profile/oidcuserinfo/}). 185 | * @async 186 | * @param {string} token 3-legged authentication token. 187 | * @returns {Promise} User profile information. 188 | */ 189 | async getUserProfile(token: string): Promise { 190 | const headers = { 191 | 'Authorization': `Bearer ${token}` 192 | }; 193 | const resp = await axios.get('https://api.userprofile.autodesk.com/userinfo', { headers }); 194 | return resp.data; 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ### Fixed 11 | - Updated dependencies (adm-zip, axios, form-data, fs-extra, mocha, node-polyfill-webpack-plugin, typedoc, @types/node) to patch some security vulnerabilities. 12 | 13 | ## [10.0.1] - 2024-04-10 14 | 15 | ## [10.0.0] - 2024-04-10 16 | 17 | ### Changed 18 | - Model derivative downloads now use the new direct-s3 logic. 19 | - Migrated to Authentication API v2 20 | 21 | ## [9.0.0] - 2022-08-19 22 | 23 | ### Changed 24 | - Uploading/downloading of OSS objects now uses the new binary transfer endpoints. 25 | - Resumable upload methods (`uploadObjectResumable`, `uploadObjectStreamResumable`, `getResumableUploadStatus`) 26 | are now marked as deprecated, and will be removed in the next major release. Use `getUploadUrls` and `completeUpload` instead. 27 | - New `useCdn` flag when generating signed URLs (true by default). 28 | 29 | ## [8.4.0] - 2022-07-04 30 | 31 | ### Added 32 | - Querying derivative trees and properties for specific object ID 33 | 34 | ## [8.3.5] - 2022-01-05 35 | 36 | ### Added 37 | - Support for filtering BIM360 issues by `status` and `assigned_to` 38 | 39 | ## [8.3.4] - 2021-11-01 40 | 41 | ### Added 42 | - Support for specifying `receiver` of DA app bundle and activity aliases 43 | 44 | ## [8.3.3] - 2021-09-27 45 | 46 | ### Added 47 | - The `ForgeClient` base class is now included in the library index 48 | 49 | ### Fixed 50 | - URL-encoding when paging through OSS entities 51 | 52 | ## [8.3.2] - 2021-03-09 53 | 54 | ### Fixed 55 | - Object uploading now allows large files 56 | 57 | ## [8.3.1] - 2021-02-04 58 | 59 | ### Fixed 60 | - Fixed Github Actions build and NPM publishing 61 | 62 | ## [8.3.0] - 2021-02-04 63 | 64 | ### Added 65 | - Method for retrieving info about BIM 360 folders 66 | - Including parent folder info in BIM 360 items 67 | 68 | ### Fixed 69 | - Updated dependencies 70 | 71 | ### Changed 72 | - Moved CI/CD to Github Actions 73 | 74 | ## [8.2.2] - 2021-01-06 75 | 76 | ### Fixed 77 | - Updated dependencies 78 | 79 | ## [8.2.1] - 2020-11-20 80 | 81 | ### Added 82 | - Downloading Model Derivative assets in chunks using the Range header 83 | 84 | ### Fixed 85 | - Removed deprecated utility code 86 | - Upgraded dependency versions 87 | 88 | ## [8.2.0] - 2020-10-29 89 | 90 | ### Added 91 | - Support for specifying workflow ID/attributes (for Forge Webhooks) in Model Derivative jobs. 92 | - Support for additional output types in Model Derivative jobs. 93 | 94 | ## [8.1.7] - 2020-09-11 95 | 96 | ### Added 97 | - Latest version of Data Management items now included in the response (kudos to https://github.com/liskaj). 98 | 99 | ### Fixed 100 | - 3rd party dependencies. 101 | 102 | ## [8.1.6] - 2020-08-12 103 | 104 | ### Fixed 105 | - Hub/project names are now retrieved properly. 106 | 107 | ## [8.1.5] - 2020-07-21 108 | 109 | ### Fixed 110 | - Couple of minor fixes in the BIM360 client by [mazerab](https://github.com/mazerab) 111 | 112 | ## [8.1.4] - 2020-07-14 113 | 114 | ### Added 115 | - Support for uploading to BIM360 Docs (kudos to [mazerab](https://github.com/mazerab)) 116 | 117 | ## [8.1.3] - 2020-07-02 118 | 119 | ### Added 120 | - Various BIM360 methods now support an additional parameter specifying user ID for 2-legged contexts (kudos to https://github.com/mazerab). 121 | - Getting BIM360 item details now returns additional information (kudos to https://github.com/liskaj). 122 | 123 | ## [8.1.2] - 2020-06-12 124 | 125 | ### Fixed 126 | - Operations on OSS objects now always URI-encode object names. 127 | 128 | ## [8.1.1] - 2020-06-03 129 | 130 | ### Fixed 131 | - Broken pagination of BIM360/DM APIs. 132 | 133 | ## [8.1.0] - 2020-05-18 134 | 135 | ### Added 136 | - Initial support for Reality Capture APIs (kudos to [mazerab](https://github.com/mazerab)) 137 | 138 | ## [8.0.14] - 2020-04-02 139 | 140 | ### Fixed 141 | - Hubs item details now including derivative URN (kudos to [liskaj](http://github.com/liskaj)!) 142 | 143 | ## [8.0.13] - 2020-01-28 144 | 145 | ### Fixed 146 | - NPM token for publishing now encrypted into Travis config 147 | 148 | ## [8.0.12] - 2020-01-28 149 | 150 | ### Changed 151 | - POST/PUT/PATCH requests no longer limited by 10MB size 152 | - Additional settings for Design Automation activities 153 | 154 | ### Fixed 155 | - Upgraded dependencies to resolve audit warnings 156 | 157 | ## [8.0.11] - 2019-12-10 158 | 159 | ### Added 160 | - Support for getting/setting/deleting Design Automation v3 nicknames 161 | 162 | ## [8.0.10] - 2019-12-05 163 | 164 | ### Fixed 165 | - Endpoint when requesting details of Data Management item version (thanks @AlexPiro!) 166 | 167 | ## [8.0.9] - 2019-12-05 168 | 169 | ### Added 170 | - Support for filtering BIM360 issues by owner 171 | 172 | ## [8.0.8] - 2019-11-21 173 | 174 | ### Added 175 | - Support for retrieving various Model Derivative data as readable stream 176 | 177 | ## [8.0.7] - 2019-11-20 178 | 179 | ### Added 180 | - Design Automation now supports its own availability regions 181 | 182 | ## [8.0.6] - 2019-11-08 183 | 184 | ### Added 185 | - Getting details of items in BIM360 data management 186 | 187 | ## [8.0.5] - 2019-11-07 188 | 189 | ### Added 190 | - BIM360 location pagination 191 | 192 | ## [8.0.4] - 2019-11-06 193 | 194 | ### Added 195 | - Getting BIM360 location container ID 196 | - Listing of BIM360 locations 197 | 198 | ## [8.0.3] - 2019-11-05 199 | 200 | ### Added 201 | - Pagination of BIM360 issues 202 | - Pagination of issue comments, attachments, and root causes 203 | 204 | ## [8.0.2] - 2019-11-05 205 | 206 | ### Added 207 | - Listing BIM360 users 208 | - Searching BIM360 users using filters 209 | 210 | ## [8.0.1] - 2019-11-04 211 | 212 | ### Added 213 | - Retrieving BIM360 issue container IDs 214 | - Support for issue filtering 215 | 216 | ## [8.0.0] - 2019-11-02 217 | 218 | ### Added 219 | - Refreshing tokens 220 | - Listing, creating, updating BIM 360 issues, their comments, attachments, issue types, root causes, etc. 221 | 222 | ### Changed 223 | - Renamed other BIM 360 methods to be consistend with other clients 224 | - Added pagination support to other BIM 360 methods 225 | 226 | ## [7.3.0] - 2019-11-01 227 | 228 | ### Added 229 | - Basic support for listing BIM360 issues and issue types 230 | - Note: the BIM360 client is **experimental**! It needs a lot of cleanup (pagination, restructuring the response JSON, etc.) 231 | 232 | ### Fixed 233 | - Missing '/' in 3-legged redirect URL. 234 | - Response when retrieving user profile 235 | 236 | ## [7.2.1] - 2019-10-17 237 | 238 | ### Fixed 239 | - Added missing token scope 240 | 241 | ## [7.2.0] - 2019-10-17 242 | 243 | ### Added 244 | - Deleting buckets 245 | 246 | ## [7.1.2] - 2019-10-09 247 | 248 | ### Fixed 249 | - Typo when retrieving webhook ID 250 | 251 | ## [7.1.1] - 2019-10-09 252 | 253 | ### Added 254 | - Helper methods for mapping webhook systems to events, and events to scopes 255 | 256 | ### Fixed 257 | - When creating a single webhook, the method only returns the webhook ID, not the entire object 258 | 259 | ## [7.1.0] - 2019-10-09 260 | 261 | ### Added 262 | - Listing & enumerating webhooks 263 | - Creating, updating, and deleting webhooks 264 | 265 | ## [7.0.0] - 2019-10-07 266 | 267 | ### Removed 268 | - Utilities for parsing SVFs and writing glTF 269 | - These are now available in a standalone package [forge-convert-utils](https://www.npmjs.com/package/forge-convert-utils) 270 | 271 | ## [6.6.0] - 2019-10-04 272 | 273 | ### Added 274 | - Basic support for serializing SVF content into glTF (2.0) 275 | 276 | ## [6.5.0] - 2019-10-04 277 | 278 | ### Added 279 | - Parsing entire SVF into memory 280 | 281 | ## [6.4.0] - 2019-10-04 282 | 283 | ### Added 284 | - Listing SVF image assets 285 | - Parsing and querying SVF property database 286 | 287 | ## [6.3.0] - 2019-10-03 288 | 289 | ### Added 290 | - Parsing lines and points from SVFs 291 | 292 | ## [6.2.0] - 2019-09-26 293 | 294 | ### Added 295 | - Higher-level utility class for parsing SVF both from local file system, 296 | and from Model Derivative service 297 | - Support for extracting additional assets embedded in the root SVF file 298 | 299 | ## [6.1.0] - 2019-09-25 300 | 301 | ### Added 302 | - Typings for parsed SVF materials 303 | - Note: breaking change of the `parseMaterials` function signature 304 | - Support for parsing more material properties & textures 305 | 306 | ## [6.0.2] - 2019-09-24 307 | 308 | ### Fixed 309 | - SVF materials only parsed when available 310 | 311 | ## [6.0.1] - 2019-09-20 312 | 313 | ### Fixed 314 | - SVF utility for parsing meshes now returns the expected number of objects 315 | 316 | ## [6.0.0] - 2019-09-20 317 | 318 | ### Added 319 | - Support for downloading SVFs to local folder 320 | 321 | ### Fixed 322 | - Removed unnecessary `async` from SVF utils (note that this is a breaking change) 323 | 324 | ## [5.3.0] - 2019-09-19 325 | 326 | ### Added 327 | - More updates and code documentation for SVF parsing utilities 328 | 329 | ## [5.2.0] - 2019-09-18 330 | 331 | ### Added 332 | - Support for streamed upload and download 333 | - New helper class for searching through Model Derivative manifests 334 | - Improved typings for Model Derivative manifests and derivatives, so better intellisense! 335 | - Initial support for parsing SVF files (see src/svf/README.md for details) 336 | 337 | ## [5.1.0] - 2019-09-12 338 | 339 | ### Added 340 | - Support for copying objects within OSS bucket 341 | 342 | ## [5.0.0] - 2019-09-03 343 | 344 | ### Changed 345 | - Methods and interfaces for creating/updating Design Automation objects 346 | - Activities can now define multiple command lines and multiple app bundles 347 | - Interfaces now better reflect the expected structure of DA inputs/outputs 348 | 349 | ## [4.4.0] - 2019-08-30 350 | 351 | ### Added 352 | - Helper method for uploading app bundle archives (`DesignAutomationClient.uploadAppBundleArchive`) 353 | 354 | ### Fixed 355 | - Creating/updating app bundles now returns the right type (with upload params) 356 | 357 | ## [4.1.2] - 2019-07-31 358 | 359 | ### Added 360 | - Simple example of using the browser bundle 361 | - Utility function for converting IDs to URNs 362 | - Generating code docs using [typedoc](https://github.com/TypeStrong/typedoc) 363 | 364 | ## [4.1.0] - 2019-07-31 365 | 366 | ### Added 367 | - Bundling into a library for browsers 368 | 369 | ### Changed 370 | - Renamed project from _forge-nodejs-utils_ to _forge-server-utils_ 371 | 372 | ## [4.0.0] - 2019-07-29 373 | 374 | ### Changed 375 | - Replaced all HTTP communication with [axios](https://github.com/axios/axios). 376 | -------------------------------------------------------------------------------- /src/reality-capture.ts: -------------------------------------------------------------------------------- 1 | import FormData from 'form-data'; 2 | import * as querystring from 'querystring'; 3 | import { BaseClient, IAuthOptions } from './common'; 4 | 5 | const ReadTokenScopes = ['data:read']; 6 | const WriteTokenScopes = ['data:write']; 7 | 8 | /** 9 | * The reconstruction engine version. 10 | * Default version is 3.0 11 | */ 12 | export enum EngineVersion { 13 | VersionOne = '1.0', 14 | VersionTwo = '2.0', 15 | VersionThree = '3.0' 16 | } 17 | 18 | export enum FileType { 19 | Image = 'image', 20 | Survey = 'survey' 21 | } 22 | 23 | /** 24 | * Specifies the GPS coordinates type. 25 | * Note: This parameter is available only if scenetype is set to aerial. 26 | */ 27 | export enum GpsType { 28 | Regular = 'regular', 29 | Precise = 'precise' 30 | } 31 | 32 | /** 33 | * Enumeration for supported photoscene output formats 34 | * .rcm: Autodesk Recap Photo Mesh (default) 35 | * .rcs: Autodesk Recap Point Cloud^ 36 | * .obj: Wavefront Object 37 | * .fbx: Autodesk FBX 3D asset exchange format 38 | * .ortho: Ortho Photo and Elevation Map^ 39 | * .report: Quality Report^ 40 | * ^ These parameter values are available only if scenetype is set to aerial. 41 | */ 42 | export enum OutputFormat { 43 | RecapPhotoMesh = 'rcm', 44 | RecapPointCloud = 'rcs', 45 | WavefrontObject = 'obj', 46 | FBXExchangeFormat = 'fbx', 47 | OrthoPhotoElevationMap = 'ortho', 48 | QualityReport = 'report' 49 | } 50 | 51 | /** 52 | * Specifies the subject type of the photoscene. 53 | */ 54 | export enum SceneType { 55 | Aerial = 'aerial', 56 | Object = 'object' 57 | } 58 | 59 | export interface IFile { 60 | fileid: string; 61 | filename: string; 62 | filesize: string; 63 | msg: string; 64 | } 65 | 66 | export interface IFiles { 67 | photosceneid: string; 68 | Files: { 69 | file: IFile[]; 70 | } 71 | } 72 | 73 | export interface IPhotoScene { 74 | Photoscene: { 75 | photosceneid: string; 76 | } 77 | } 78 | 79 | export interface IPhotoSceneCancelDelete { 80 | msg: string; 81 | } 82 | 83 | export interface IPhotoSceneError { 84 | code: number; 85 | msg: string; 86 | } 87 | 88 | export interface IPhotoSceneOptions { 89 | scenename: string; 90 | callback?: string; 91 | format?: OutputFormat; 92 | scenetype?: SceneType; 93 | gpstype?: GpsType; 94 | hubprojectid?: string; 95 | hubfolderid?: string; 96 | version?: EngineVersion; 97 | metadataname?: string[]; 98 | metadatavalue?: string[]; 99 | } 100 | 101 | export interface IPhotoSceneOutput { 102 | Photoscene: { 103 | filesize: string; 104 | photosceneid: string; 105 | progress: string; 106 | progressmsg: string; 107 | resultmsg: string; 108 | scenelink: string; 109 | urn: string; 110 | } 111 | } 112 | 113 | export interface IPhotoSceneProcess { 114 | msg: string; 115 | Photoscene: { 116 | photosceneid: string; 117 | } 118 | } 119 | 120 | export interface IPhotoSceneProgress { 121 | photosceneid: string; 122 | progressmsg: string; 123 | progress: string; 124 | } 125 | 126 | /** 127 | * Client providing access to Autodesk Platform Services {@link https://aps.autodesk.com/en/docs/reality-capture/v1/developers_guide/overview|reality capture APIs}. 128 | * @tutorial realitycapture 129 | */ 130 | export class RealityCaptureClient extends BaseClient { 131 | /** 132 | * Initializes new client with specific authentication method 133 | * @param {IAuthOptions} auth Authentication object, 134 | * containing `client_id` and `client_secret` properties (for 2-legged authentication). 135 | * @param {string} [host="https://developer.api.autodesk.com"] APS host. 136 | */ 137 | constructor(auth: IAuthOptions, host?: string) { 138 | super('photo-to-3d/v1/', auth, host); 139 | } 140 | 141 | /** 142 | * Creates new photoscene 143 | * {@link https://aps.autodesk.com/en/docs/reality-capture/v1/reference/http/photoscene-POST|docs}. 144 | * @async 145 | * @param {IPhotoSceneOptions} options Specifies the parameters for the new photoscene. 146 | * @returns {Promise} A JSON object containing details of the photoscene that was created, with property 'photosceneid' ID of the photoscene that was created. 147 | * @throws Error when the request fails, for example, due to invalid request. 148 | */ 149 | async createPhotoScene(options: IPhotoSceneOptions): Promise { 150 | const params: any = { 151 | scenename: options.scenename 152 | } 153 | if (options.callback) { 154 | params.callback = options.callback; 155 | } 156 | if (options.format) { 157 | params.format = options.format; 158 | } 159 | if (options.scenetype) { 160 | params.scenetype = options.scenetype; 161 | } 162 | if (options.scenetype !== 'object' && options.gpstype) { 163 | params.gpstype = options.gpstype; 164 | } 165 | if (options.hubprojectid && options.hubfolderid) { 166 | params.hubprojectid = options.hubprojectid; 167 | params.hubfolderid = options.hubfolderid; 168 | } 169 | if (options.version) { 170 | params.version = options.version; 171 | } 172 | if ( 173 | options.scenetype !== 'object' 174 | && options.metadataname 175 | && options.metadatavalue 176 | && options.metadataname.length > 0 177 | && options.metadatavalue.length > 0 178 | && options.metadataname.length === options.metadatavalue.length 179 | ) { 180 | for (let i=0; i} A JSON object containing details of the image files uploaded to the photoscene. 203 | * @throws Error when the request fails, for example, due to invalid request. 204 | */ 205 | async addImages(photosceneid: string, type: FileType, files: Buffer[]): Promise { 206 | const form = new FormData(); 207 | form.append('photosceneid', photosceneid); 208 | form.append('type', type); 209 | for (let i=0; i} A JSON object containing details of the image files uploaded to the photoscene. 230 | * @throws Error when the request fails, for example, due to invalid request. 231 | */ 232 | async addImageURLs(photosceneid: string, type: FileType, files: string[]): Promise { 233 | const params: any = { 234 | photosceneid, 235 | type 236 | }; 237 | for (let i=0; i} A JSON object containing a message for current processing job. 254 | * @throws Error when the request fails, for example, due to invalid request. 255 | */ 256 | async processPhotoScene(photosceneid: string): Promise { 257 | const headers: { [key: string]: string } = {}; 258 | headers['Content-Type'] = 'application/x-www-form-urlencoded'; 259 | return this.post(`photoscene/${photosceneid}`, {}, headers, WriteTokenScopes); 260 | } 261 | 262 | /** 263 | * Returns the processing progress and status of a photoscene. 264 | * @async 265 | * @param {string} photosceneid Specifies the ID of the photoscene to retrieve status 266 | * @returns {Promise} A JSON object containing details of current progress status. 267 | * @throws Error when the request fails, for example, due to invalid request. 268 | */ 269 | async getPhotoSceneProgress(photosceneid: string): Promise { 270 | return this.get(`photoscene/${photosceneid}/progress`, {}, ReadTokenScopes); 271 | } 272 | 273 | /** 274 | * Returns a time-limited HTTPS link to an output file of the specified format. 275 | * Note: The link will expire 30 days after the date of processing completion. 276 | * @async 277 | * @param {string} photosceneid Specifies the ID of the photoscene to download the output 278 | * @returns {Promise} A JSON object containing time-bound HTTPS link to the output file. 279 | * @throws Error when the request fails, for example, due to invalid request. 280 | */ 281 | async getPhotoScene(photosceneid: string, format: OutputFormat): Promise { 282 | return this.get(`photoscene/${photosceneid}?format=${format}`, {}, ReadTokenScopes); 283 | } 284 | 285 | /** 286 | * Aborts the processing of a photoscene and marks it as cancelled. 287 | * @async 288 | * @param {string} photosceneid Specifices the ID of the photoscene to cancel. 289 | * @returns {IPhotoSceneCancelDelete|IPhotoSceneError} A JSON object containing information on cancelled job. 290 | * @throws Error when the request fails, for example, due to invalid request. 291 | */ 292 | async cancelPhotoScene(photosceneid: string): Promise { 293 | const headers: { [key: string]: string } = {}; 294 | headers['Content-Type'] = 'application/x-www-form-urlencoded'; 295 | return this.post(`photoscene/${photosceneid}/cancel`, {}, headers, WriteTokenScopes); 296 | } 297 | 298 | /** 299 | * Deletes a photoscene and its associated assets (images, output files, ...). 300 | * @async 301 | * @param {string} photosceneid Specifices the ID of the photoscene to delete. 302 | * @returns {IPhotoSceneCancelDelete|IPhotoSceneError} A JSON object containing information on deleted job. 303 | * @throws Error when the request fails, for example, due to invalid request. 304 | */ 305 | async deletePhotoScene(photosceneid: string): Promise { 306 | const headers: { [key: string]: string } = {}; 307 | headers['Content-Type'] = 'application/x-www-form-urlencoded'; 308 | return this.delete(`photoscene/${photosceneid}`, headers, WriteTokenScopes); 309 | } 310 | 311 | } -------------------------------------------------------------------------------- /src/webhooks.ts: -------------------------------------------------------------------------------- 1 | import { BaseClient, IAuthOptions, Region } from './common'; 2 | import { AxiosRequestConfig } from 'axios'; 3 | 4 | const ReadTokenScopes = ['data:read']; 5 | const WriteTokenScopes = ['data:read', 'data:write']; 6 | 7 | /** 8 | * Available webhook systems. 9 | */ 10 | export enum WebhookSystem { 11 | Data = 'data', 12 | Derivative = 'derivative', 13 | RevitCloudWorksharing = 'adsk.c4r', 14 | FusionLifecycle = 'adsk.flc.production' 15 | } 16 | 17 | /** 18 | * Available webhook events. 19 | * Note that only certain events can be used with specific systems, for example, 20 | * `WebhookEvent.Data*` values can only be used with `WebhookSystem.Data`. 21 | */ 22 | export enum WebhookEvent { 23 | DataVersionAdded = 'dm.version.added', 24 | DataVersionModified = 'dm.version.modified', 25 | DataVersionDeleted = 'dm.version.deleted', 26 | DataVersionMoved = 'dm.version.moved', 27 | DataVersionCopied = 'dm.version.copied', 28 | DataFolderAdded = 'dm.folder.added', 29 | DataFolderModified = 'dm.folder.modified', 30 | DataFolderDeleted = 'dm.folder.deleted', 31 | DataFolderMoved = 'dm.folder.moved', 32 | DataFolderCopied = 'dm.folder.copied', 33 | 34 | DerivativeExtractionFinished = 'extraction.finished', 35 | DerivativeExtractionUpdated = 'extraction.updated', 36 | 37 | RevitModelPublish = 'model.publish', 38 | RevitModelSync = 'model.sync', 39 | 40 | FusionItemClone = 'item.clone', 41 | FusionItemCreate = 'item.create', 42 | FusionItemLock = 'item.lock', 43 | FusionItemRelease = 'item.release', 44 | FusionItemUnlock = 'item.unlock', 45 | FusionItemUpdate = 'item.update', 46 | FusionWorkflowTransition = 'workflow.transition' 47 | } 48 | 49 | /** 50 | * Webhook status. 51 | */ 52 | export enum WebhookStatus { 53 | Active = 'active', 54 | Inactive = 'inactive' 55 | } 56 | 57 | export type WebhookScope = { folder: string; } | { workflow: string; } | { workspace: string; } | { 'workflow.transition': string; }; 58 | 59 | /** 60 | * List all event types available for specific webhook system. 61 | * @param {WebhookSystem} system Webhook system (e.g. "data"). 62 | * @returns {WebhookEvent[]} List of webhook events. 63 | */ 64 | export function webhookSystemEvents(system: WebhookSystem): WebhookEvent[] { 65 | switch (system) { 66 | case WebhookSystem.Data: 67 | return [ 68 | WebhookEvent.DataFolderAdded, 69 | WebhookEvent.DataFolderCopied, 70 | WebhookEvent.DataFolderDeleted, 71 | WebhookEvent.DataFolderModified, 72 | WebhookEvent.DataFolderMoved, 73 | WebhookEvent.DataVersionAdded, 74 | WebhookEvent.DataVersionCopied, 75 | WebhookEvent.DataVersionDeleted, 76 | WebhookEvent.DataVersionModified, 77 | WebhookEvent.DataVersionMoved 78 | ]; 79 | case WebhookSystem.Derivative: 80 | return [ 81 | WebhookEvent.DerivativeExtractionUpdated, 82 | WebhookEvent.DerivativeExtractionFinished 83 | ]; 84 | case WebhookSystem.FusionLifecycle: 85 | return [ 86 | WebhookEvent.FusionItemClone, 87 | WebhookEvent.FusionItemCreate, 88 | WebhookEvent.FusionItemLock, 89 | WebhookEvent.FusionItemRelease, 90 | WebhookEvent.FusionItemUnlock, 91 | WebhookEvent.FusionItemUpdate, 92 | WebhookEvent.FusionWorkflowTransition 93 | ]; 94 | case WebhookSystem.RevitCloudWorksharing: 95 | return [ 96 | WebhookEvent.RevitModelPublish, 97 | WebhookEvent.RevitModelSync 98 | ]; 99 | } 100 | } 101 | 102 | /** 103 | * List all scope keys available for specific webhook event. 104 | * @param {WebhookEvent} event Webhook event (e.g., "dm.folder.moved"). 105 | * @returns {string[]} List of scope names that can be used when creating or updating a webhook. 106 | */ 107 | export function webhookEventScopes(event: WebhookEvent): string[] { 108 | switch (event) { 109 | case WebhookEvent.DataVersionAdded: 110 | case WebhookEvent.DataVersionModified: 111 | case WebhookEvent.DataVersionDeleted: 112 | case WebhookEvent.DataVersionMoved: 113 | case WebhookEvent.DataVersionCopied: 114 | case WebhookEvent.DataFolderAdded: 115 | case WebhookEvent.DataFolderModified: 116 | case WebhookEvent.DataFolderDeleted: 117 | case WebhookEvent.DataFolderMoved: 118 | case WebhookEvent.DataFolderCopied: 119 | return [ 120 | 'folder' 121 | ]; 122 | case WebhookEvent.DerivativeExtractionFinished: 123 | case WebhookEvent.DerivativeExtractionUpdated: 124 | return [ 125 | 'workflow' 126 | ]; 127 | case WebhookEvent.RevitModelPublish: 128 | case WebhookEvent.RevitModelSync: 129 | return [ 130 | 'folder' 131 | ]; 132 | case WebhookEvent.FusionItemClone: 133 | case WebhookEvent.FusionItemCreate: 134 | case WebhookEvent.FusionItemLock: 135 | case WebhookEvent.FusionItemRelease: 136 | case WebhookEvent.FusionItemUnlock: 137 | case WebhookEvent.FusionItemUpdate: 138 | return [ 139 | 'workspace' 140 | ]; 141 | case WebhookEvent.FusionWorkflowTransition: 142 | return [ 143 | 'workflow.transition' 144 | ]; 145 | } 146 | } 147 | 148 | /** 149 | * Webhook descriptor. 150 | */ 151 | export interface IWebhook { 152 | hookId: string; 153 | tenant: string; 154 | callbackUrl: string; 155 | createdBy: string; 156 | event: string; 157 | createdDate: string; 158 | system: string; 159 | creatorType: string; 160 | status: WebhookStatus; 161 | scope: WebhookScope; 162 | urn: string; 163 | } 164 | 165 | /** 166 | * Parameters when creating a webhook. 167 | */ 168 | export interface ICreateWebhookParams { 169 | /** 170 | * Callback URL registered for the webhook. 171 | */ 172 | callbackUrl: string; 173 | /** 174 | * An object that represents the extent to where the event is monitored. 175 | * For example, if the scope is folder, the webhooks service generates a notification 176 | * for the specified event occurring in any sub folder or item within that folder. 177 | */ 178 | scope: WebhookScope; 179 | /** 180 | * A user-defined JSON object, which you can use to store/set some custom information. 181 | * The maximum size of the JSON object (content) should be less than 1KB. 182 | */ 183 | hookAttribute?: object; 184 | /** 185 | * JsonPath expression (for example, "$[?(@.ext=='txt')]") that can be used 186 | * to filter the callbacks you receive. 187 | */ 188 | filter?: string; 189 | } 190 | 191 | /** 192 | * Parameters when updating a webhook. 193 | * Undefined properties are ignored, and null values 194 | * can be used to clear the configuration property of webhook. 195 | */ 196 | export interface IUpdateWebhookParams { 197 | /** 198 | * Webhook status (can be either 'active' or 'inactive'). 199 | */ 200 | status?: WebhookStatus; 201 | /** 202 | * JsonPath expression (for example, "$[?(@.ext=='txt')]") that can be used 203 | * to filter the callbacks you receive. 204 | */ 205 | filter?: string | null; 206 | /** 207 | * A user-defined JSON object, which you can use to store/set some custom information. 208 | * The maximum size of the JSON object (content) should be less than 1KB. 209 | */ 210 | hookAttribute?: object | null; 211 | } 212 | 213 | /** 214 | * Client providing access to Autodesk Platform Services {@link https://aps.autodesk.com/en/docs/webhooks/v1/developers_guide/overview|webhooks APIs}. 215 | * @tutorial webhooks 216 | */ 217 | export class WebhooksClient extends BaseClient { 218 | /** 219 | * Initializes new client with specific authentication method. 220 | * @param {IAuthOptions} auth Authentication object, 221 | * containing either `client_id` and `client_secret` properties (for 2-legged authentication), 222 | * or a single `token` property (for 2-legged or 3-legged authentication with pre-generated access token). 223 | * @param {string} [host="https://developer.api.autodesk.com"] APS host. 224 | * @param {Region} [region="US"] APS availability region ("US" or "EMEA"). 225 | */ 226 | constructor(auth: IAuthOptions, host?: string, region?: Region) { 227 | super('webhooks/v1', auth, host, region); 228 | } 229 | 230 | // Iterates (asynchronously) over pages of paginated results 231 | private async *_pager(endpoint: string) { 232 | let response = await this.get(endpoint, {}, ReadTokenScopes); 233 | yield response.data; 234 | 235 | while (response.links && response.links.next) { 236 | const next = new URL(response.links.next); 237 | const pageState = next.searchParams.get('pageState') || ''; 238 | response = await this.get(`${endpoint}${endpoint.indexOf('?') === -1 ? '?' : '&'}pageState=${pageState}`, {}, ReadTokenScopes); 239 | yield response.data; 240 | } 241 | } 242 | 243 | // Collects all pages of paginated results 244 | private async _collect(endpoint: string) { 245 | let response = await this.get(endpoint, {}, ReadTokenScopes); 246 | let results = response.data; 247 | 248 | while (response.links && response.links.next) { 249 | const next = new URL(response.links.next); 250 | const pageState = next.searchParams.get('pageState') || ''; 251 | response = await this.get(`${endpoint}${endpoint.indexOf('?') === -1 ? '?' : '&'}pageState=${pageState}`, {}, ReadTokenScopes); 252 | results = results.concat(response.items); 253 | } 254 | return results; 255 | } 256 | 257 | /** 258 | * Iterates over all webhooks, webhooks for specific system, or webhooks for specific system and event 259 | * ({@link https://aps.autodesk.com/en/docs/webhooks/v1/reference/http/hooks-GET|docs}, 260 | * {@link https://aps.autodesk.com/en/docs/webhooks/v1/reference/http/systems-system-hooks-GET|docs}, 261 | * {@link https://aps.autodesk.com/en/docs/webhooks/v1/reference/http/systems-system-events-event-hooks-GET|docs}). 262 | * @async 263 | * @generator 264 | * @param {WebhookSystem} [system] Optional webhook system (e.g., "data") to filter the results. 265 | * @param {WebhookEvent} [event] Optional webhook event (e.g., "dm.version.copied") to filter the results. 266 | * @yields {AsyncIterable} Single page of webhooks. 267 | * @throws Error when the request fails, for example, due to insufficient rights, or incorrect scopes. 268 | */ 269 | async *iterateHooks(system?: WebhookSystem, event?: WebhookEvent): AsyncIterable { 270 | let endpoint = `hooks?region=${this.region}`; 271 | if (system && event) { 272 | endpoint = `systems/${system}/events/${event}/` + endpoint; 273 | } else if (system) { 274 | endpoint = `systems/${system}/` + endpoint; 275 | } 276 | for await (const hooks of this._pager(endpoint)) { 277 | yield hooks; 278 | } 279 | } 280 | 281 | /** 282 | * Lists all webhooks, webhooks for specific system, or webhooks for specific system and event 283 | * ({@link https://aps.autodesk.com/en/docs/webhooks/v1/reference/http/hooks-GET|docs}, 284 | * {@link https://aps.autodesk.com/en/docs/webhooks/v1/reference/http/systems-system-hooks-GET|docs}, 285 | * {@link https://aps.autodesk.com/en/docs/webhooks/v1/reference/http/systems-system-events-event-hooks-GET|docs}). 286 | * @async 287 | * @param {WebhookSystem} [system] Optional webhook system (e.g., "data") to filter the results. 288 | * @param {WebhookEvent} [event] Optional webhook event (e.g., "dm.version.copied") to filter the results. 289 | * @returns {Promise} List of all webhooks. 290 | * @throws Error when the request fails, for example, due to insufficient rights, or incorrect scopes. 291 | */ 292 | async listHooks(system?: WebhookSystem, event?: WebhookEvent): Promise { 293 | let endpoint = `hooks?region=${this.region}`; 294 | if (system && event) { 295 | endpoint = `systems/${system}/events/${event}/` + endpoint; 296 | } else if (system) { 297 | endpoint = `systems/${system}/` + endpoint; 298 | } 299 | return this._collect(endpoint); 300 | } 301 | 302 | /** 303 | * Provides details about a specific webhook 304 | * ({@link https://aps.autodesk.com/en/docs/webhooks/v1/reference/http/systems-system-events-event-hooks-hook_id-GET|docs}). 305 | * @async 306 | * @param {WebhookSystem} system Webhook system (e.g., "data"). 307 | * @param {WebhookEvent} event Webhook event (e.g., "dm.version.copied"). 308 | * @param {string} id Webhook ID. 309 | * @returns {Promise} Webhook details. 310 | * @throws Error when the request fails, for example, due to insufficient rights, or incorrect scopes. 311 | */ 312 | async getHookDetails(system: WebhookSystem, event: WebhookEvent, id: string): Promise { 313 | const hook = await this.get(`systems/${system}/events/${event}/hooks/${id}?region=${this.region}`, {}, ReadTokenScopes); 314 | return hook; 315 | } 316 | 317 | /** 318 | * Creates new webhook, either for entire webhook system, or for a specific event 319 | * ({@link https://aps.autodesk.com/en/docs/webhooks/v1/reference/http/systems-system-hooks-POST|docs}, 320 | * {@link https://aps.autodesk.com/en/docs/webhooks/v1/reference/http/systems-system-events-event-hooks-POST|docs}). 321 | * @param {WebhookSystem} system Webhook system (e.g., "data"). 322 | * @param {WebhookEvent | undefined} event Optional webhook event (e.g., "dm.version.copied"). 323 | * If undefined, the webhook will be defined for the entire webhook system. 324 | * @param {ICreateWebhookParams} params Parameters of the new webhook. 325 | * @returns {Promise} Webhook ID (when both `system` and `event` parameters are provided). 326 | * or a list of webhooks (when only `system` is specified). 327 | * @throws Error when the request fails, for example, due to insufficient rights, or incorrect scopes. 328 | */ 329 | async createHook(system: WebhookSystem, event: WebhookEvent | undefined, params: ICreateWebhookParams): Promise { 330 | const endpoint = event 331 | ? `systems/${system}/events/${event}/hooks?region=${this.region}` 332 | : `systems/${system}/hooks?region=${this.region}`; 333 | const config: AxiosRequestConfig = {}; 334 | await this.setAuthorization(config, WriteTokenScopes); 335 | const response = await this.axios.post(endpoint, params, config); 336 | if (response.data.hooks) { 337 | return response.data.hooks as IWebhook[]; 338 | } else { 339 | const location = response.headers['location'] || response.headers['Location']; 340 | const tokens = location.split('/'); 341 | return tokens[tokens.length - 1]; 342 | } 343 | } 344 | 345 | /** 346 | * Updates an existing webhook 347 | * ({@link https://aps.autodesk.com/en/docs/webhooks/v1/reference/http/systems-system-events-event-hooks-hook_id-PATCH|docs}). 348 | * @async 349 | * @param {WebhookSystem} system Webhook system (e.g., "data"). 350 | * @param {WebhookEvent} event Webhook event (e.g., "dm.version.copied"). 351 | * @param {string} id Webhook ID. 352 | * @param {IUpdateWebhookParams} params Parameters to update. Undefined properties are ignored, 353 | * and "null" values can be used to clear the specific configuration of the webhook. 354 | */ 355 | async updateHook(system: WebhookSystem, event: WebhookEvent, id: string, params: IUpdateWebhookParams) { 356 | await this.patch(`systems/${system}/events/${event}/hooks/${id}?region=${this.region}`, params, {}, WriteTokenScopes); 357 | } 358 | 359 | /** 360 | * Deletes a webhook 361 | * ({@link https://aps.autodesk.com/en/docs/webhooks/v1/reference/http/systems-system-events-event-hooks-hook_id-DELETE|docs}). 362 | * @async 363 | * @param {WebhookSystem} system Webhook system (e.g., "data"). 364 | * @param {WebhookEvent} event Webhook event (e.g., "dm.version.copied"). 365 | * @throws Error when the request fails, for example, due to insufficient rights, or incorrect scopes. 366 | */ 367 | async deleteHook(system: WebhookSystem, event: WebhookEvent, id: string) { 368 | await this.delete(`systems/${system}/events/${event}/hooks/${id}?region=${this.region}`, {}, WriteTokenScopes); 369 | } 370 | } 371 | -------------------------------------------------------------------------------- /src/model-derivative.ts: -------------------------------------------------------------------------------- 1 | import { Readable } from 'stream'; 2 | 3 | import { BaseClient, IAuthOptions, Region, sleep } from './common'; 4 | 5 | const isNullOrUndefined = (value: any) => value === null || value === undefined; 6 | 7 | const RootPath = 'modelderivative/v2'; 8 | const ReadTokenScopes = ['data:read']; 9 | const WriteTokenScopes = ['data:read', 'data:write', 'data:create']; 10 | const RetryDelay = 5000; 11 | 12 | /** 13 | * Converts ID of an object to base64-encoded URN expected by {@link ModelDerivativeClient}. 14 | * @param {string} id Object ID. 15 | * @returns {string} base64-encoded object URN. 16 | * @example 17 | * urnify('urn:adsk.objects:os.object:my-bucket/my-file.dwg'); 18 | * // Returns 'dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6bXktYnVja2V0L215LWZpbGUuZHdn' 19 | */ 20 | export function urnify(id: string): string { 21 | return Buffer.from(id).toString('base64').replace(/=/g, ''); 22 | } 23 | 24 | export interface IDerivativeFormats { 25 | [outputFormat: string]: string[]; 26 | } 27 | 28 | export type IDerivativeOutputType = IDerivativeOutputTypeSVF 29 | | IDerivativeOutputTypeSVF2 30 | | IDerivativeOutputTypeSTL 31 | | IDerivativeOutputTypeSTEP 32 | | IDerivativeOutputTypeIGES 33 | | IDerivativeOutputTypeOBJ 34 | | IDerivativeOutputTypeDWG 35 | | IDerivativeOutputTypeIFC; 36 | 37 | export interface IDerivativeOutputTypeSVF { 38 | type: 'svf', 39 | views: string[]; 40 | advanced?: { 41 | switchLoader?: boolean; 42 | conversionMethod?: string; 43 | buildingStoreys?: string; 44 | spaces?: string; 45 | openingElements?: string; 46 | generateMasterViews?: boolean; 47 | materialMode?: string; 48 | hiddenObjects?: boolean; 49 | basicMaterialProperties?: boolean; 50 | autodeskMaterialProperties?: boolean; 51 | timelinerProperties?: boolean; 52 | }; 53 | } 54 | 55 | export interface IDerivativeOutputTypeSVF2 { 56 | type: 'svf2', 57 | views: string[]; 58 | advanced?: { 59 | switchLoader?: boolean; 60 | conversionMethod?: string; 61 | buildingStoreys?: string; 62 | spaces?: string; 63 | openingElements?: string; 64 | generateMasterViews?: boolean; 65 | materialMode?: string; 66 | hiddenObjects?: boolean; 67 | basicMaterialProperties?: boolean; 68 | autodeskMaterialProperties?: boolean; 69 | timelinerProperties?: boolean; 70 | }; 71 | } 72 | 73 | export interface IDerivativeOutputTypeSTL { 74 | type: 'stl', 75 | advanced?: { 76 | format?: string; 77 | exportColor?: boolean; 78 | exportFileStructure?: string; 79 | }; 80 | } 81 | 82 | export interface IDerivativeOutputTypeSTEP { 83 | type: 'step', 84 | advanced?: { 85 | applicationProtocol?: string; 86 | tolerance?: number; 87 | }; 88 | } 89 | 90 | export interface IDerivativeOutputTypeIGES { 91 | type: 'iges', 92 | advanced?: { 93 | tolerance?: number; 94 | surfaceType?: string; 95 | sheetType?: string; 96 | solidType?: string; 97 | }; 98 | } 99 | 100 | export interface IDerivativeOutputTypeOBJ { 101 | type: 'obj', 102 | advanced?: { 103 | exportFileStructure?: string; 104 | unit?: string; 105 | modelGuid?: string; 106 | objectIds?: number[]; 107 | }; 108 | } 109 | 110 | export interface IDerivativeOutputTypeDWG { 111 | type: 'dwg', 112 | advanced?: { 113 | exportSettingName?: string; 114 | }; 115 | } 116 | 117 | export interface IDerivativeOutputTypeIFC { 118 | type: 'ifc', 119 | advanced?: { 120 | exportSettingName?: string; 121 | }; 122 | } 123 | 124 | export interface IJob { 125 | result: string; 126 | urn: string; 127 | //acceptedJobs?: any; 128 | //output?: any; 129 | } 130 | 131 | export interface IDerivativeManifest { 132 | type: string; 133 | hasThumbnail: string; 134 | status: string; 135 | progress: string; 136 | region: string; 137 | urn: string; 138 | version: string; 139 | derivatives: IDerivative[]; 140 | } 141 | 142 | export interface IDerivative { 143 | status: string; 144 | progress?: string; 145 | name?: string; 146 | hasThumbnail?: string; 147 | outputType?: string; 148 | children?: DerivativeChild[]; 149 | } 150 | 151 | type DerivativeChild = IDerivativeResourceChild | IDerivativeGeometryChild | IDerivativeViewChild; 152 | 153 | export interface IDerivativeChild { 154 | guid: string; 155 | type: string; 156 | role: string; 157 | status: string; 158 | progress?: string; 159 | children?: DerivativeChild[]; 160 | } 161 | 162 | export interface IDerivativeResourceChild extends IDerivativeChild { 163 | type: 'resource'; 164 | urn: string; 165 | mime: string; 166 | } 167 | 168 | export interface IDerivativeGeometryChild extends IDerivativeChild { 169 | type: 'geometry'; 170 | name?: string; 171 | viewableID?: string; 172 | phaseNames?: string; 173 | hasThumbnail?: string; 174 | properties?: any; 175 | } 176 | 177 | export interface IDerivativeViewChild extends IDerivativeChild { 178 | type: 'view'; 179 | name?: string; 180 | camera?: number[]; 181 | viewbox?: number[]; 182 | } 183 | 184 | export interface IDerivativeMetadata { 185 | // TODO 186 | } 187 | 188 | export interface IDerivativeTree { 189 | // TODO 190 | } 191 | 192 | export interface IDerivativeProps { 193 | // TODO 194 | } 195 | 196 | export enum ThumbnailSize { 197 | Small = 100, 198 | Medium = 200, 199 | Large = 400 200 | } 201 | 202 | interface IDerivativeDownloadInfo { 203 | etag: string; 204 | size: number; 205 | url: string; 206 | 'content-type': string; 207 | expiration: number; 208 | cookies: { [key: string]: string }; 209 | } 210 | 211 | /** 212 | * Utility class for querying {@see IDerivativeManifest}. 213 | */ 214 | export class ManifestHelper { 215 | constructor(protected manifest: IDerivativeManifest) {} 216 | 217 | /** 218 | * Finds manifest derivatives with matching 'guid', 'type', or 'role' properties. 219 | * @param {object} query Dictionary of the requested properties and values. 220 | * @returns {DerivativeChild[]} Matching derivatives. 221 | */ 222 | search(query: { guid?: string; type?: string; role?: string; }): DerivativeChild[] { 223 | let matches: DerivativeChild[] = []; 224 | this.traverse((child: DerivativeChild) => { 225 | if ((isNullOrUndefined(query.guid) || child.guid === query.guid) 226 | && (isNullOrUndefined(query.type) || child.type === query.type) 227 | && (isNullOrUndefined(query.role) || child.role === query.role)) { 228 | matches.push(child); 229 | } 230 | return true; 231 | }); 232 | return matches; 233 | } 234 | 235 | /** 236 | * Traverses all derivatives, executing the input callback for each one. 237 | * @param {(child: DerivativeChild) => boolean} callback Function to be called for each derivative, 238 | * returning a bool indicating whether the traversal should recurse deeper in the manifest hierarchy. 239 | */ 240 | traverse(callback: (child: DerivativeChild) => boolean) { 241 | function process(node: DerivativeChild, callback: (child: DerivativeChild) => boolean) { 242 | const proceed = callback(node); 243 | if (proceed && node.children) { 244 | for (const child of node.children) { 245 | process(child, callback); 246 | } 247 | } 248 | } 249 | for (const derivative of this.manifest.derivatives) { 250 | if (derivative.children) { 251 | for (const child of derivative.children) { 252 | process(child, callback); 253 | } 254 | } 255 | } 256 | } 257 | } 258 | 259 | /** 260 | * Client providing access to Autodesk Platform Services 261 | * {@link https://aps.autodesk.com/en/docs/model-derivative/v2|model derivative APIs}. 262 | * @tutorial model-derivative 263 | */ 264 | export class ModelDerivativeClient extends BaseClient { 265 | /** 266 | * Initializes new client with specific authentication method. 267 | * @param {IAuthOptions} auth Authentication object, 268 | * containing either `client_id` and `client_secret` properties (for 2-legged authentication), 269 | * or a single `token` property (for 2-legged or 3-legged authentication with pre-generated access token). 270 | * @param {string} [host="https://developer.api.autodesk.com"] APS host. 271 | * @param {Region} [region="US"] APS availability region. 272 | */ 273 | constructor(auth: IAuthOptions, host?: string, region?: Region) { 274 | super(RootPath, auth, host, region); 275 | } 276 | 277 | private getUrl(path: string): URL { 278 | return new URL(this.host + '/' + RootPath + '/' + path); 279 | } 280 | 281 | /** 282 | * Gets a list of supported translation formats 283 | * ({@link https://aps.autodesk.com/en/docs/model-derivative/v2/reference/http/formats-GET|docs}). 284 | * @async 285 | * @yields {Promise} Dictionary of all supported output formats 286 | * mapped to arrays of formats these outputs can be obtained from. 287 | * @throws Error when the request fails, for example, due to insufficient rights. 288 | */ 289 | async formats(): Promise { 290 | const response = await this.get('designdata/formats', {}, ReadTokenScopes); 291 | return response.formats; 292 | } 293 | 294 | /** 295 | * Submits a translation job 296 | * ({@link https://aps.autodesk.com/en/docs/model-derivative/v2/reference/http/job-POST|docs}). 297 | * @async 298 | * @param {string} urn Document to be translated. 299 | * @param {IDerivativeOutputType[]} outputs List of requested output formats. 300 | * @param {string} [pathInArchive] Optional relative path to root design if the translated file is an archive. 301 | * @param {boolean} [force] Force translation even if a derivative already exists. 302 | * @param {string} [workflowId] Optional workflow ID to be used with APS Webhooks. 303 | * @param {object} [workflowAttr] Optional workflow attributes to be used with APS Webhooks. 304 | * @returns {Promise} Translation job details, with properties 'result', 305 | * 'urn', and 'acceptedJobs'. 306 | * @throws Error when the request fails, for example, due to insufficient rights. 307 | */ 308 | async submitJob(urn: string, outputs: IDerivativeOutputType[], pathInArchive?: string, force?: boolean, workflowId?: string, workflowAttr?: object): Promise { 309 | const params: any = { 310 | input: { 311 | urn: urn 312 | }, 313 | output: { 314 | formats: outputs, 315 | destination: { 316 | region: this.region 317 | } 318 | } 319 | }; 320 | if (pathInArchive) { 321 | params.input.compressedUrn = true; 322 | params.input.rootFilename = pathInArchive; 323 | } 324 | if (workflowId) { 325 | params.misc = { 326 | workflow: workflowId 327 | }; 328 | if (workflowAttr) { 329 | params.misc.workflowAttribute = workflowAttr; 330 | } 331 | } 332 | const headers: { [key: string]: string } = {}; 333 | if (force) { 334 | headers['x-ads-force'] = 'true'; 335 | } 336 | return this.post('designdata/job', params, headers, WriteTokenScopes); 337 | } 338 | 339 | /** 340 | * Retrieves manifest of a derivative 341 | * ({@link https://aps.autodesk.com/en/docs/model-derivative/v2/reference/http/urn-manifest-GET|docs}). 342 | * @async 343 | * @param {string} urn Document derivative URN. 344 | * @returns {Promise} Document derivative manifest. 345 | * @throws Error when the request fails, for example, due to insufficient rights. 346 | */ 347 | async getManifest(urn: string): Promise { 348 | return this.get(this.region === Region.EMEA ? `regions/eu/designdata/${urn}/manifest` : `designdata/${urn}/manifest`, {}, ReadTokenScopes); 349 | } 350 | 351 | /** 352 | * Deletes manifest 353 | * ({@link https://aps.autodesk.com/en/docs/model-derivative/v2/reference/http/urn-manifest-DELETE|docs}). 354 | * @async 355 | * @param {string} urn Document derivative URN. 356 | * @throws Error when the request fails, for example, due to insufficient rights, or incorrect scopes. 357 | */ 358 | async deleteManifest(urn: string) { 359 | return this.delete(this.region === Region.EMEA ? `regions/eu/designdata/${urn}/manifest` : `designdata/${urn}/manifest`, {}, WriteTokenScopes); 360 | } 361 | 362 | // Generates URL for downloading specific derivative 363 | // https://aps.autodesk.com/en/docs/model-derivative/v2/reference/http/urn-manifest-derivativeUrn-signedcookies-GET 364 | protected async getDerivativeDownloadUrl(modelUrn: string, derivativeUrn: string): Promise { 365 | const endpoint = this.region === Region.EMEA 366 | ? `regions/eu/designdata/${modelUrn}/manifest/${derivativeUrn}/signedcookies` 367 | : `designdata/${modelUrn}/manifest/${derivativeUrn}/signedcookies`; 368 | const config = {}; 369 | await this.setAuthorization(config, ReadTokenScopes); 370 | const resp = await this.axios.get(endpoint, config); 371 | const record: IDerivativeDownloadInfo = { 372 | etag: resp.data.etag, 373 | size: resp.data.size, 374 | url: resp.data.url, 375 | 'content-type': resp.data['content-type'], 376 | expiration: resp.data.expiration, 377 | cookies: {} 378 | }; 379 | if (!resp || !resp.headers || !resp.headers['set-cookie']) { 380 | return record; 381 | } 382 | for (const cookie of resp.headers['set-cookie']) { 383 | const tokens = cookie.split(';'); 384 | const [key, val] = tokens[0].trim().split('='); 385 | record.cookies[key] = val; 386 | } 387 | return record; 388 | } 389 | 390 | /** 391 | * Downloads content of a specific model derivative 392 | * ({@link https://aps.autodesk.com/en/docs/model-derivative/v2/reference/http/urn-manifest-derivativeurn-GET/|docs}). 393 | * @async 394 | * @param {string} modelUrn Model URN. 395 | * @param {string} derivativeUrn Derivative URN. 396 | * @returns {Promise} Derivative content. 397 | * @throws Error when the request fails, for example, due to insufficient rights, or incorrect scopes. 398 | */ 399 | async getDerivative(modelUrn: string, derivativeUrn: string): Promise { 400 | const downloadInfo = await this.getDerivativeDownloadUrl(modelUrn, derivativeUrn); 401 | const resp = await this.axios.get(downloadInfo.url, { 402 | responseType: 'arraybuffer', 403 | decompress: false, 404 | headers: { 405 | Cookie: Object.keys(downloadInfo.cookies).map(key => `${key}=${downloadInfo.cookies[key]}`).join(';') 406 | } 407 | }); 408 | return resp.data; 409 | } 410 | 411 | /** 412 | * Downloads content of a specific model derivative 413 | * ({@link https://aps.autodesk.com/en/docs/model-derivative/v2/reference/http/urn-manifest-derivativeurn-GET/|docs}). 414 | * @async 415 | * @param {string} modelUrn Model URN. 416 | * @param {string} derivativeUrn Derivative URN. 417 | * @returns {Promise} Derivative content stream. 418 | * @throws Error when the request fails, for example, due to insufficient rights, or incorrect scopes. 419 | */ 420 | async getDerivativeStream(modelUrn: string, derivativeUrn: string): Promise { 421 | const downloadInfo = await this.getDerivativeDownloadUrl(modelUrn, derivativeUrn); 422 | const resp = await this.axios.get(downloadInfo.url, { 423 | responseType: 'stream', 424 | decompress: false 425 | }); 426 | return resp.data; 427 | } 428 | 429 | /** 430 | * Downloads content of a specific model derivative asset in chunks 431 | * ({@link https://aps.autodesk.com/en/docs/model-derivative/v2/reference/http/urn-manifest-derivativeurn-GET/|docs}). 432 | * @param {string} modelUrn Model URN. 433 | * @param {string} derivativeUrn Derivative URN. 434 | * @param {number} [maxChunkSize=1<<24] Maximum size (in bytes) of a single downloaded chunk. 435 | * @returns {Readable} Readable stream with the content of the downloaded derivative asset. 436 | * @throws Error when the request fails, for example, due to insufficient rights, or incorrect scopes. 437 | */ 438 | getDerivativeChunked(modelUrn: string, derivativeUrn: string, maxChunkSize: number = 1 << 24): Readable { 439 | const client = this; 440 | async function * read() { 441 | const downloadInfo = await client.getDerivativeDownloadUrl(modelUrn, derivativeUrn); 442 | const contentLength = downloadInfo.size; 443 | let resp = await client.axios.head(downloadInfo.url); 444 | let streamedBytes = 0; 445 | while (streamedBytes < contentLength) { 446 | const chunkSize = Math.min(maxChunkSize, contentLength - streamedBytes); 447 | resp = await client.axios.get(downloadInfo.url, { 448 | responseType: 'arraybuffer', 449 | decompress: false, 450 | headers: { 451 | Range: `bytes=${streamedBytes}-${streamedBytes + chunkSize - 1}` 452 | } 453 | }); 454 | yield resp.data; 455 | streamedBytes += chunkSize; 456 | } 457 | } 458 | return Readable.from(read()); 459 | } 460 | 461 | /** 462 | * Retrieves metadata of a derivative 463 | * ({@link https://aps.autodesk.com/en/docs/model-derivative/v2/reference/http/urn-metadata-GET|docs}). 464 | * @async 465 | * @param {string} urn Document derivative URN. 466 | * @returns {Promise} Document derivative metadata. 467 | * @throws Error when the request fails, for example, due to insufficient rights. 468 | */ 469 | async getMetadata(urn: string): Promise { 470 | return this.get(this.region === Region.EMEA ? `regions/eu/designdata/${urn}/metadata` : `designdata/${urn}/metadata`, {}, ReadTokenScopes); 471 | } 472 | 473 | /** 474 | * Retrieves metadata of a derivative as a readable stream 475 | * ({@link https://aps.autodesk.com/en/docs/model-derivative/v2/reference/http/urn-metadata-GET|docs}). 476 | * @async 477 | * @param {string} urn Document derivative URN. 478 | * @returns {Promise} Document derivative metadata. 479 | * @throws Error when the request fails, for example, due to insufficient rights. 480 | */ 481 | async getMetadataStream(urn: string): Promise { 482 | return this.getStream(this.region === Region.EMEA ? `regions/eu/designdata/${urn}/metadata` : `designdata/${urn}/metadata`, {}, ReadTokenScopes); 483 | } 484 | 485 | /** 486 | * Retrieves object tree of a specific viewable 487 | * ({@link https://aps.autodesk.com/en/docs/model-derivative/v2/reference/http/urn-metadata-guid-GET|docs}). 488 | * @async 489 | * @param {string} urn Document derivative URN. 490 | * @param {string} guid Viewable GUID. 491 | * @param {boolean} [force] Force query even when exceeding the size limit (20MB). 492 | * @param {number} [objectId] If specified, retrieves the sub-tree that has the specified object ID as its parent node. 493 | * If this parameter is not specified, retrieves the entire object tree. 494 | * @param {boolean} [retryOn202] Keep repeating the request while the response status is 202 (indicating that the resource is being prepared). 495 | * @returns {Promise} Viewable object tree. 496 | * @throws Error when the request fails, for example, due to insufficient rights. 497 | */ 498 | async getViewableTree(urn: string, guid: string, force?: boolean, objectId?: number, retryOn202: boolean = true): Promise { 499 | const url = this.getUrl(this.region === Region.EMEA ? `regions/eu/designdata/${urn}/metadata/${guid}` : `designdata/${urn}/metadata/${guid}`); 500 | if (force) 501 | url.searchParams.append('forceget', 'true'); 502 | if (objectId) 503 | url.searchParams.append('objectid', objectId.toString()); 504 | const config = {}; 505 | await this.setAuthorization(config, ReadTokenScopes); 506 | let resp = await this.axios.get(url.toString(), config); 507 | while (resp.status === 202 && retryOn202) { 508 | await sleep(RetryDelay); 509 | await this.setAuthorization(config, ReadTokenScopes); 510 | resp = await this.axios.get(url.toString(), config); 511 | } 512 | return resp.data; 513 | } 514 | 515 | /** 516 | * Retrieves object tree of a specific viewable as a readable stream 517 | * ({@link https://aps.autodesk.com/en/docs/model-derivative/v2/reference/http/urn-metadata-guid-GET|docs}). 518 | * @async 519 | * @param {string} urn Document derivative URN. 520 | * @param {string} guid Viewable GUID. 521 | * @param {boolean} [force] Force query even when exceeding the size limit (20MB). 522 | * @param {number} [objectId] If specified, retrieves the sub-tree that has the specified object ID as its parent node. 523 | * If this parameter is not specified, retrieves the entire object tree. 524 | * @param {boolean} [retryOn202] Keep repeating the request while the response status is 202 (indicating that the resource is being prepared). 525 | * @returns {Promise} Readable stream. 526 | * @throws Error when the request fails, for example, due to insufficient rights. 527 | */ 528 | async getViewableTreeStream(urn: string, guid: string, force?: boolean, objectId?: number, retryOn202: boolean = true): Promise { 529 | const url = this.getUrl(this.region === Region.EMEA ? `regions/eu/designdata/${urn}/metadata/${guid}` : `designdata/${urn}/metadata/${guid}`); 530 | if (force) 531 | url.searchParams.append('forceget', 'true'); 532 | if (objectId) 533 | url.searchParams.append('objectid', objectId.toString()); 534 | const config: any = { responseType: 'stream' }; 535 | await this.setAuthorization(config, ReadTokenScopes); 536 | let resp = await this.axios.get(url.toString(), config); 537 | while (resp.status === 202 && retryOn202) { 538 | await sleep(RetryDelay); 539 | await this.setAuthorization(config, ReadTokenScopes); 540 | resp = await this.axios.get(url.toString(), config); 541 | } 542 | return resp.data; 543 | } 544 | 545 | /** 546 | * Retrieves properties of a specific viewable 547 | * ({@link https://aps.autodesk.com/en/docs/model-derivative/v2/reference/http/urn-metadata-guid-properties-GET|docs}). 548 | * @async 549 | * @param {string} urn Document derivative URN. 550 | * @param {string} guid Viewable GUID. 551 | * @param {boolean} [force] Force query even when exceeding the size limit (20MB). 552 | * @param {number} [objectId] The Object ID of the object you want to query properties for. 553 | * If `objectid` is omitted, the server returns properties for all objects. 554 | * @param {boolean} [retryOn202] Keep repeating the request while the response status is 202 (indicating that the resource is being prepared). 555 | * @returns {Promise} Viewable properties. 556 | * @throws Error when the request fails, for example, due to insufficient rights. 557 | */ 558 | async getViewableProperties(urn: string, guid: string, force?: boolean, objectId?: number, retryOn202: boolean = true): Promise { 559 | const url = this.getUrl(this.region === Region.EMEA ? `regions/eu/designdata/${urn}/metadata/${guid}/properties` : `designdata/${urn}/metadata/${guid}/properties`); 560 | if (force) 561 | url.searchParams.append('forceget', 'true'); 562 | if (objectId) 563 | url.searchParams.append('objectid', objectId.toString()); 564 | const config: any = {}; 565 | await this.setAuthorization(config, ReadTokenScopes); 566 | let resp = await this.axios.get(url.toString(), config); 567 | while (resp.status === 202 && retryOn202) { 568 | await sleep(RetryDelay); 569 | await this.setAuthorization(config, ReadTokenScopes); 570 | resp = await this.axios.get(url.toString(), config); 571 | } 572 | return resp.data; 573 | } 574 | 575 | /** 576 | * Retrieves properties of a specific viewable as a readable stream 577 | * ({@link https://aps.autodesk.com/en/docs/model-derivative/v2/reference/http/urn-metadata-guid-properties-GET|docs}). 578 | * @async 579 | * @param {string} urn Document derivative URN. 580 | * @param {string} guid Viewable GUID. 581 | * @param {boolean} [force] Force query even when exceeding the size limit (20MB). 582 | * @param {number} [objectId] The Object ID of the object you want to query properties for. 583 | * If `objectid` is omitted, the server returns properties for all objects. 584 | * @param {boolean} [retryOn202] Keep repeating the request while the response status is 202 (indicating that the resource is being prepared). 585 | * @returns {Promise} Readable stream. 586 | * @throws Error when the request fails, for example, due to insufficient rights. 587 | */ 588 | async getViewablePropertiesStream(urn: string, guid: string, force?: boolean, objectId?: number, retryOn202: boolean = true): Promise { 589 | const url = this.getUrl(this.region === Region.EMEA ? `regions/eu/designdata/${urn}/metadata/${guid}/properties` : `designdata/${urn}/metadata/${guid}/properties`); 590 | if (force) 591 | url.searchParams.append('forceget', 'true'); 592 | if (objectId) 593 | url.searchParams.append('objectid', objectId.toString()); 594 | const config: any = { responseType: 'stream' }; 595 | await this.setAuthorization(config, ReadTokenScopes); 596 | let resp = await this.axios.get(url.toString(), config); 597 | while (resp.status === 202 && retryOn202) { 598 | await sleep(RetryDelay); 599 | await this.setAuthorization(config, ReadTokenScopes); 600 | resp = await this.axios.get(url.toString(), config); 601 | } 602 | return resp.data; 603 | } 604 | 605 | /** 606 | * Retrieves derivative thumbnail 607 | * ({@link https://aps.autodesk.com/en/docs/model-derivative/v2/reference/http/urn-thumbnail-GET|docs}). 608 | * @async 609 | * @param {string} urn Document derivative URN. 610 | * @param {ThumbnailSize} [size=ThumbnailSize.Medium] Thumbnail size (small: 100x100 px, medium: 200x200 px, or large: 400x400 px). 611 | * @returns {Promise} Thumbnail data. 612 | * @throws Error when the request fails, for example, due to insufficient rights, or incorrect scopes. 613 | */ 614 | async getThumbnail(urn: string, size: ThumbnailSize = ThumbnailSize.Medium): Promise { 615 | const endpoint = this.region === Region.EMEA ? `regions/eu/designdata/${urn}/thumbnail` : `designdata/${urn}/thumbnail`; 616 | return this.getBuffer(endpoint + '?width=' + size, {}, ReadTokenScopes); 617 | } 618 | 619 | /** 620 | * Retrieves derivative thumbnail stream 621 | * ({@link https://aps.autodesk.com/en/docs/model-derivative/v2/reference/http/urn-thumbnail-GET|docs}). 622 | * @async 623 | * @param {string} urn Document derivative URN. 624 | * @param {ThumbnailSize} [size=ThumbnailSize.Medium] Thumbnail size (small: 100x100 px, medium: 200x200 px, or large: 400x400 px). 625 | * @returns {Promise} Thumbnail data stream. 626 | * @throws Error when the request fails, for example, due to insufficient rights, or incorrect scopes. 627 | */ 628 | async getThumbnailStream(urn: string, size: ThumbnailSize = ThumbnailSize.Medium): Promise { 629 | const endpoint = this.region === Region.EMEA ? `regions/eu/designdata/${urn}/thumbnail` : `designdata/${urn}/thumbnail`; 630 | return this.getStream(endpoint + '?width=' + size, {}, ReadTokenScopes); 631 | } 632 | } 633 | -------------------------------------------------------------------------------- /src/data-management.ts: -------------------------------------------------------------------------------- 1 | import { BaseClient, IAuthOptions, Region, sleep } from './common'; 2 | import { AxiosError, AxiosRequestConfig } from 'axios'; 3 | 4 | const RootPath = 'oss/v2'; 5 | const ReadTokenScopes = ['bucket:read', 'data:read']; 6 | const WriteTokenScopes = ['bucket:create', 'bucket:delete', 'data:write']; 7 | 8 | export interface IBucket { 9 | bucketKey: string; 10 | createdDate: number; 11 | policyKey: string; 12 | } 13 | 14 | export interface IBucketPermission { 15 | authId: string; 16 | access: string; 17 | } 18 | 19 | export interface IBucketDetail extends IBucket { 20 | bucketOwner: string; 21 | permissions: IBucketPermission[]; 22 | } 23 | 24 | export enum DataRetentionPolicy { 25 | Transient = 'transient', 26 | Temporary = 'temporary', 27 | Persistent = 'persistent' 28 | } 29 | 30 | export interface IUploadParams { 31 | urls: string[]; 32 | uploadKey: string; 33 | } 34 | 35 | export interface IDownloadParams { 36 | /** Indicates status of the object */ 37 | status: 'complete' | 'chunked' | 'fallback'; 38 | /** The S3 signed URL to download from. This attribute is returned when the value 39 | * of the status attribute is complete or fallback (in which case the URL will be 40 | * an OSS Signed URL instead of an S3 signed URL). 41 | */ 42 | url?: string; 43 | /** A map of S3 signed URLs where each key correspond to a specific byte range chunk. 44 | * This attribute is returned when the value of the status attribute is chunked. 45 | */ 46 | urls?: { [range: string]: string }; 47 | /** The values for the updatable params that were used in the creation of the returned 48 | * S3 signed URL (`Content-Type`, `Content-Disposition` & `Cache-Control`). 49 | */ 50 | params?: object; 51 | /** The object size in bytes. */ 52 | size?: number; 53 | /** The calculated SHA-1 hash of the object, if available. */ 54 | sha1?: string; 55 | } 56 | 57 | export interface IObject { 58 | objectKey: string; 59 | bucketKey: string; 60 | objectId: string; 61 | sha1: string; 62 | size: number; 63 | location: string; 64 | } 65 | 66 | export interface IResumableUploadRange { 67 | start: number; 68 | end: number; 69 | } 70 | 71 | export interface ISignedUrl { 72 | signedUrl: string; 73 | expiration: number; 74 | singleUse: boolean; 75 | } 76 | 77 | export interface IUploadOptions { 78 | contentType?: string; 79 | progress?: (bytesUploaded: number, totalBytes?: number) => void; 80 | //cancel?: () => boolean; 81 | } 82 | 83 | export interface IDownloadOptions { 84 | contentType?: string; 85 | progress?: (bytesDownloaded: number, totalBytes?: number) => void; 86 | //cancel?: () => boolean; 87 | } 88 | 89 | type Task = () => Promise; 90 | 91 | /** 92 | * Executes a list of asynchronous tasks, running up to {@see concurrency} tasks at the same time. 93 | * @param tasks Async tasks to execute. 94 | * @param concurrency Max number of tasks to run at the same time. 95 | * @returns Promise that is resolved when all tasks have completed, or rejected when some of the tasks fail. 96 | */ 97 | function parallelize(tasks: Task[], concurrency: number = 5): Promise { 98 | const queue = tasks.slice(); 99 | let remaining = queue.length; 100 | return new Promise(function (resolve, reject) { 101 | function onTaskCompleted() { 102 | remaining--; 103 | if (remaining === 0) { 104 | resolve(); 105 | } else if (queue.length > 0) { 106 | const task = queue.shift() as Task; 107 | task().then(onTaskCompleted).catch(onTaskFailed); 108 | } 109 | } 110 | function onTaskFailed(reason: any) { 111 | reject(reason); 112 | } 113 | for (let i = 0; i < concurrency && queue.length > 0; i++) { 114 | const task = queue.shift() as Task; 115 | task().then(onTaskCompleted).catch(onTaskFailed); 116 | } 117 | }); 118 | } 119 | 120 | /** 121 | * Client providing access to APS Data Management API ({@link https://aps.autodesk.com/en/docs/data/v2/reference/http/}). 122 | * @tutorial data-management 123 | */ 124 | export class DataManagementClient extends BaseClient { 125 | /** 126 | * Initializes new client with specific authentication method. 127 | * @param {IAuthOptions} auth Authentication object, 128 | * containing either `client_id` and `client_secret` properties (for 2-legged authentication), 129 | * or a single `token` property (for 2-legged or 3-legged authentication with pre-generated access token). 130 | * @param {string} [host="https://developer.api.autodesk.com"] APS host. 131 | * @param {Region} [region="US"] APS availability region ("US" or "EMEA"). 132 | */ 133 | constructor(auth: IAuthOptions, host?: string, region?: Region) { 134 | super(RootPath, auth, host, region); 135 | } 136 | 137 | // Iterates (asynchronously) over pages of paginated results 138 | private async *_pager(endpoint: string, limit: number) { 139 | let response = await this.get(`${endpoint}${endpoint.indexOf('?') === -1 ? '?' : '&'}limit=${limit}`, {}, ReadTokenScopes); 140 | yield response.items; 141 | 142 | while (response.next) { 143 | const next = new URL(response.next); 144 | const startAt = next.searchParams.get('startAt') || ''; 145 | response = await this.get(`${endpoint}${endpoint.indexOf('?') === -1 ? '?' : '&'}startAt=${encodeURIComponent(startAt)}&limit=${limit}`, {}, ReadTokenScopes); 146 | yield response.items; 147 | } 148 | } 149 | 150 | // Collects all pages of paginated results 151 | private async _collect(endpoint: string) { 152 | let response = await this.get(endpoint, {}, ReadTokenScopes); 153 | let results = response.items; 154 | 155 | while (response.next) { 156 | const next = new URL(response.next); 157 | const startAt = next.searchParams.get('startAt') || ''; 158 | response = await this.get(`${endpoint}${endpoint.indexOf('?') === -1 ? '?' : '&'}startAt=${encodeURIComponent(startAt)}`, {}, ReadTokenScopes); 159 | results = results.concat(response.items); 160 | } 161 | return results; 162 | } 163 | 164 | // Bucket APIs 165 | 166 | /** 167 | * Iterates over all buckets in pages of predefined size 168 | * ({@link https://aps.autodesk.com/en/docs/data/v2/reference/http/buckets-GET|docs}). 169 | * @async 170 | * @generator 171 | * @param {number} [limit=16] Max number of buckets to receive in one batch (allowed values: 1-100). 172 | * @yields {AsyncIterable} List of bucket object containing 'bucketKey', 'createdDate', and 'policyKey'. 173 | * @throws Error when the request fails, for example, due to insufficient rights, or incorrect scopes. 174 | */ 175 | async *iterateBuckets(limit: number = 16): AsyncIterable { 176 | for await (const buckets of this._pager(`buckets?region=${this.region}`, limit)) { 177 | yield buckets; 178 | } 179 | } 180 | 181 | /** 182 | * Lists all buckets 183 | * ({@link https://aps.autodesk.com/en/docs/data/v2/reference/http/buckets-GET|docs}). 184 | * @async 185 | * @returns {Promise} List of bucket objects. 186 | * @throws Error when the request fails, for example, due to insufficient rights, or incorrect scopes. 187 | */ 188 | async listBuckets(): Promise { 189 | return this._collect(`buckets?region=${this.region}`); 190 | } 191 | 192 | /** 193 | * Gets details of a specific bucket 194 | * ({@link https://aps.autodesk.com/en/docs/data/v2/reference/http/buckets-:bucketKey-details-GET|docs}). 195 | * @async 196 | * @param {string} bucket Bucket key. 197 | * @returns {Promise} Bucket details, with properties "bucketKey", "bucketOwner", "createdDate", 198 | * "permissions", and "policyKey". 199 | * @throws Error when the request fails, for example, due to insufficient rights, or when a bucket 200 | * with this name does not exist. 201 | */ 202 | async getBucketDetails(bucket: string): Promise { 203 | return this.get(`buckets/${bucket}/details`, {}, ReadTokenScopes); 204 | } 205 | 206 | /** 207 | * Creates a new bucket 208 | * ({@link https://aps.autodesk.com/en/docs/data/v2/reference/http/buckets-POST|docs}). 209 | * @async 210 | * @param {string} bucket Bucket key. 211 | * @param {DataRetentionPolicy} dataRetention Data retention policy for objects uploaded to this bucket. 212 | * @returns {Promise} Bucket details, with properties "bucketKey", "bucketOwner", "createdDate", 213 | * "permissions", and "policyKey". 214 | * @throws Error when the request fails, for example, due to insufficient rights, incorrect scopes, 215 | * or when a bucket with this name already exists. 216 | */ 217 | async createBucket(bucket: string, dataRetention: DataRetentionPolicy): Promise { 218 | const params = { bucketKey: bucket, policyKey: dataRetention }; 219 | return this.post('buckets', params, { 'x-ads-region': this.region }, WriteTokenScopes); 220 | } 221 | 222 | // Object APIs 223 | 224 | /** 225 | * Iterates over all objects in a bucket in pages of predefined size 226 | * ({@link https://aps.autodesk.com/en/docs/data/v2/reference/http/buckets-:bucketKey-objects-GET|docs}). 227 | * @async 228 | * @generator 229 | * @param {string} bucket Bucket key. 230 | * @param {number} [limit=16] Max number of objects to receive in one batch (allowed values: 1-100). 231 | * @param {string} [beginsWith] Optional filter to only return objects whose keys are prefixed with this value. 232 | * @yields {AsyncIterable} List of object containing 'bucketKey', 'objectKey', 'objectId', 'sha1', 'size', and 'location'. 233 | * @throws Error when the request fails, for example, due to insufficient rights, or incorrect scopes. 234 | */ 235 | async *iterateObjects(bucket: string, limit: number = 16, beginsWith?: string): AsyncIterable { 236 | let url = `buckets/${bucket}/objects`; 237 | if (beginsWith) { 238 | url += '?beginsWith=' + beginsWith; 239 | } 240 | for await (const objects of this._pager(url, limit)) { 241 | yield objects; 242 | } 243 | } 244 | 245 | /** 246 | * Lists all objects in a bucket 247 | * ({@link https://aps.autodesk.com/en/docs/data/v2/reference/http/buckets-:bucketKey-objects-GET|docs}). 248 | * @async 249 | * @param {string} bucket Bucket key. 250 | * @param {string} [beginsWith] Optional filter to only return objects whose keys are prefixed with this value. 251 | * @returns {Promise} List of object containing 'bucketKey', 'objectKey', 'objectId', 'sha1', 'size', and 'location'. 252 | * @throws Error when the request fails, for example, due to insufficient rights, or incorrect scopes. 253 | */ 254 | async listObjects(bucket: string, beginsWith?: string): Promise { 255 | let url = `buckets/${bucket}/objects`; 256 | if (beginsWith) { 257 | url += '?beginsWith=' + encodeURIComponent(beginsWith); 258 | } 259 | return this._collect(url); 260 | } 261 | 262 | /** 263 | * Generates one or more signed URLs that can be used to upload a file (or its parts) to OSS, 264 | * and an upload key that is used to generate additional URLs or in {@see completeUpload} 265 | * after all the parts have been uploaded successfully. 266 | * 267 | * The URLs are valid for 60min. 268 | * 269 | * Note that if you are uploading in multiple parts, each part except for the final one 270 | * must be of size at least 5MB, otherwise the call to {@see completeUpload} will fail. 271 | * 272 | * @param {string} bucketKey Bucket key. 273 | * @param {string} objectKey Object key. 274 | * @param {number} [parts=1] How many URLs to generate in case of multi-part upload. 275 | * @param {number} [firstPart=1] Index of the part the first returned URL should point to. 276 | * For example, to upload parts 10 through 15 of a file, use `firstPart` = 10 and `parts` = 6. 277 | * @param {string} [uploadKey] Optional upload key if this is a continuation of a previously 278 | * initiated upload. 279 | * @returns {IUploadParams} Signed URLs for uploading chunks of the file to AWS S3 (valid for 60min), 280 | * and a unique upload key used to generate additional URLs or to complete the upload. 281 | */ 282 | async getUploadUrls(bucketKey: string, objectKey: string, parts: number = 1, firstPart: number = 1, uploadKey?: string): Promise { 283 | let endpoint = `buckets/${bucketKey}/objects/${encodeURIComponent(objectKey)}/signeds3upload?parts=${parts}&firstPart=${firstPart}`; 284 | if (uploadKey) { 285 | endpoint += `&uploadKey=${uploadKey}`; 286 | } 287 | return this.get(endpoint, { 'Content-Type': 'application/json' }, WriteTokenScopes); 288 | } 289 | 290 | /** 291 | * Finalizes the upload of a file to OSS. 292 | * @param {string} bucketKey Bucket key. 293 | * @param {string} objectKey Object key. 294 | * @param {string} uploadKey Upload key returned by {@see getUploadUrls}. 295 | * @param {string} [contentType] Optinal content type that should be recorded for the uploaded file. 296 | * @returns {IObject} Details of the uploaded object in OSS. 297 | */ 298 | async completeUpload(bucketKey: string, objectKey: string, uploadKey: string, contentType?: string): Promise { 299 | const headers: { [header: string]: string } = { 'Content-Type': 'application/json' }; 300 | if (contentType) { 301 | headers['x-ads-meta-Content-Type'] = contentType; 302 | } 303 | return this.post(`buckets/${bucketKey}/objects/${encodeURIComponent(objectKey)}/signeds3upload`, { uploadKey }, headers, WriteTokenScopes); 304 | } 305 | 306 | /** 307 | * Uploads content to a specific bucket object. 308 | * @async 309 | * @param {string} bucketKey Bucket key. 310 | * @param {string} objectKey Name of uploaded object. 311 | * @param {Buffer} data Object content. 312 | * @param {IUploadOptions} [options] Additional upload options. 313 | * @returns {Promise} Object description containing 'bucketKey', 'objectKey', 'objectId', 314 | * 'sha1', 'size', 'location', and 'contentType'. 315 | * @throws Error when the request fails, for example, due to insufficient rights, or incorrect scopes. 316 | */ 317 | async uploadObject(bucketKey: string, objectKey: string, data: Buffer, options?: IUploadOptions): Promise { 318 | console.assert(data.byteLength > 0); 319 | const ChunkSize = 5 << 20; 320 | const MaxBatches = 25; 321 | const totalParts = Math.ceil(data.byteLength / ChunkSize); 322 | let partsUploaded = 0; 323 | let bytesUploaded = 0; 324 | let uploadUrls: string[] = []; 325 | let uploadKey: string | undefined; 326 | while (partsUploaded < totalParts) { 327 | const chunk = data.slice(partsUploaded * ChunkSize, Math.min((partsUploaded + 1) * ChunkSize, data.byteLength)); 328 | while (true) { 329 | if (uploadUrls.length === 0) { 330 | const uploadParams = await this.getUploadUrls(bucketKey, objectKey, Math.min(totalParts - partsUploaded, MaxBatches), partsUploaded + 1, uploadKey); // Automatically retries 429 and 500-599 responses 331 | uploadUrls = uploadParams.urls.slice(); 332 | uploadKey = uploadParams.uploadKey; 333 | } 334 | const url = uploadUrls.shift() as string; 335 | try { 336 | await this.axios.put(url, chunk); 337 | break; 338 | } catch (err) { 339 | const status = (err as AxiosError).response?.status as number; 340 | if (status === 403) { 341 | uploadUrls = []; // Couldn't this cause an infinite loop? (i.e., could the server keep responding with 403 indefinitely?) 342 | } else { 343 | throw err; 344 | } 345 | } 346 | } 347 | partsUploaded++; 348 | bytesUploaded += chunk.byteLength; 349 | if (options?.progress) { 350 | options.progress(bytesUploaded, data.byteLength); 351 | } 352 | } 353 | return this.completeUpload(bucketKey, objectKey, uploadKey as string, options?.contentType); 354 | } 355 | 356 | /** 357 | * Uploads content stream to a specific bucket object. 358 | * @async 359 | * @param {string} bucketKey Bucket key. 360 | * @param {string} objectKey Name of uploaded object. 361 | * @param {AsyncIterable} stream Input stream. 362 | * @param {IUploadOptions} [options] Additional upload options. 363 | * @returns {Promise} Object description containing 'bucketKey', 'objectKey', 'objectId', 364 | * 'sha1', 'size', 'location', and 'contentType'. 365 | * @throws Error when the request fails, for example, due to insufficient rights, or incorrect scopes. 366 | */ 367 | async uploadObjectStream(bucketKey: string, objectKey: string, input: AsyncIterable, options?: IUploadOptions): Promise { 368 | // Helper async generator making sure that each chunk has at least certain number of bytes 369 | async function* bufferChunks(input: AsyncIterable, minChunkSize: number) { 370 | let buffer = Buffer.alloc(2 * minChunkSize); 371 | let bytesRead = 0; 372 | for await (const chunk of input) { 373 | chunk.copy(buffer, bytesRead); 374 | bytesRead += chunk.byteLength; 375 | if (bytesRead >= minChunkSize) { 376 | yield buffer.slice(0, bytesRead); 377 | bytesRead = 0; 378 | } 379 | } 380 | if (bytesRead > 0) { 381 | yield buffer.slice(0, bytesRead); 382 | } 383 | } 384 | 385 | const MaxBatches = 25; 386 | const ChunkSize = 5 << 20; 387 | let partsUploaded = 0; 388 | let bytesUploaded = 0; 389 | let uploadUrls: string[] = []; 390 | let uploadKey: string | undefined; 391 | for await (const chunk of bufferChunks(input, ChunkSize)) { 392 | while (true) { 393 | if (uploadUrls.length === 0) { 394 | const uploadParams = await this.getUploadUrls(bucketKey, objectKey, MaxBatches, partsUploaded + 1, uploadKey); 395 | uploadUrls = uploadParams.urls.slice(); 396 | uploadKey = uploadParams.uploadKey; 397 | } 398 | const url = uploadUrls.shift() as string; 399 | try { 400 | await this.axios.put(url, chunk); 401 | break; 402 | } catch (err) { 403 | const status = (err as AxiosError).response?.status as number; 404 | if (status === 403) { 405 | uploadUrls = []; // Couldn't this cause an infinite loop? (i.e., could the server keep responding with 403 indefinitely? 406 | } else { 407 | throw err; 408 | } 409 | } 410 | } 411 | partsUploaded++; 412 | bytesUploaded += chunk.byteLength; 413 | if (options?.progress) { 414 | options.progress(bytesUploaded, undefined); 415 | } 416 | } 417 | return this.completeUpload(bucketKey, objectKey, uploadKey as string, options?.contentType); 418 | } 419 | 420 | /** 421 | * Generates a signed URL that can be used to download a file from OSS. 422 | * @param bucketKey Bucket key. 423 | * @param objectKey Object key. 424 | * @returns {IDownloadParams} Download URLs and potentially other helpful information. 425 | */ 426 | async getDownloadUrl(bucketKey: string, objectKey: string, useCdn: boolean = true): Promise { 427 | const endpoint = `buckets/${bucketKey}/objects/${encodeURIComponent(objectKey)}/signeds3download?useCdn=${useCdn}`; 428 | return this.get(endpoint, { 'Content-Type': 'application/json' }, ReadTokenScopes); 429 | } 430 | 431 | /** 432 | * Downloads a specific OSS object. 433 | * @async 434 | * @param {string} bucketKey Bucket key. 435 | * @param {string} objectKey Object key. 436 | * @param {IDownloadOptions} [options] Additional download options. 437 | * @returns {Promise} Object content. 438 | * @throws Error when the request fails, for example, due to insufficient rights, or incorrect scopes. 439 | */ 440 | async downloadObject(bucketKey: string, objectKey: string, options?: IDownloadOptions): Promise { 441 | const downloadParams = await this.getDownloadUrl(bucketKey, objectKey); 442 | if (downloadParams.status !== 'complete') { 443 | throw new Error('File not available for download yet.'); 444 | } 445 | const resp = await this.axios.get(downloadParams.url as string, { 446 | responseType: 'arraybuffer', 447 | onDownloadProgress: progressEvent => { 448 | const downloadedBytes = progressEvent.event.response.length; 449 | const totalBytes = parseInt(progressEvent.event.responseHeaders['Content-Length']); 450 | if (options?.progress) { 451 | options.progress(downloadedBytes, totalBytes); 452 | } 453 | } 454 | }); 455 | return resp.data; 456 | } 457 | 458 | /** 459 | * Downloads content stream of a specific bucket object. 460 | * @async 461 | * @param {string} bucketKey Bucket key. 462 | * @param {string} objectKey Object name. 463 | * @param {IDownloadOptions} [options] Additional download options. 464 | * @returns {Promise} Object content stream. 465 | * @throws Error when the request fails, for example, due to insufficient rights, or incorrect scopes. 466 | */ 467 | async downloadObjectStream(bucketKey: string, objectKey: string, options?: IDownloadOptions): Promise { 468 | const downloadParams = await this.getDownloadUrl(bucketKey, objectKey); 469 | if (downloadParams.status !== 'complete') { 470 | throw new Error('File not available for download yet.'); 471 | } 472 | const resp = await this.axios.get(downloadParams.url as string, { 473 | responseType: 'stream', 474 | onDownloadProgress: progressEvent => { 475 | const downloadedBytes = progressEvent.event.response.length; 476 | const totalBytes = parseInt(progressEvent.event.responseHeaders['Content-Length']); 477 | if (options?.progress) { 478 | options.progress(downloadedBytes, totalBytes); 479 | } 480 | } 481 | }); 482 | return resp.data; 483 | } 484 | 485 | /** 486 | * @deprecated This method of resumable upload is now deprecated and will be removed in future versions. 487 | * Use {@see getUploadUrls} and {@see completeUpload} instead. 488 | * 489 | * Uploads content to a specific bucket object using the resumable capabilities 490 | * ({@link https://aps.autodesk.com/en/docs/data/v2/reference/http/buckets-:bucketKey-objects-:objectName-resumable-PUT|docs}). 491 | * @async 492 | * @param {string} bucketKey Bucket key. 493 | * @param {string} objectName Name of uploaded object. 494 | * @param {Buffer} data Object content. 495 | * @param {number} byteOffset Byte offset of the uploaded blob in the target object. 496 | * @param {number} totalBytes Total byte size of the target object. 497 | * @param {string} sessionId Resumable session ID. 498 | * @param {string} [contentType='application/stream'] Type of content to be used in HTTP headers, for example, "application/json". 499 | * @throws Error when the request fails, for example, due to insufficient rights, or incorrect scopes. 500 | */ 501 | async uploadObjectResumable(bucketKey: string, objectName: string, data: Buffer, byteOffset: number, totalBytes: number, sessionId: string, contentType: string = 'application/stream') { 502 | console.warn('This method is deprecated and will be removed in future versions.'); 503 | const headers = { 504 | 'Authorization': '', 505 | 'Content-Type': contentType, 506 | 'Content-Length': data.byteLength.toString(), 507 | 'Content-Range': `bytes ${byteOffset}-${byteOffset + data.byteLength - 1}/${totalBytes}`, 508 | 'Session-Id': sessionId 509 | } 510 | return this.put(`buckets/${bucketKey}/objects/${encodeURIComponent(objectName)}/resumable`, data, headers, WriteTokenScopes); 511 | } 512 | 513 | /** 514 | * @deprecated This method of resumable upload is now deprecated and will be removed in future versions. 515 | * Use {@see getUploadUrls} and {@see completeUpload} instead. 516 | * 517 | * Uploads content stream to a specific bucket object using the resumable capabilities 518 | * ({@link https://aps.autodesk.com/en/docs/data/v2/reference/http/buckets-:bucketKey-objects-:objectName-resumable-PUT|docs}). 519 | * @async 520 | * @param {string} bucketKey Bucket key. 521 | * @param {string} objectName Name of uploaded object. 522 | * @param {ReadableStream} stream Object content stream. 523 | * @param {number} chunkBytes Byte size of the stream to be uploaded. 524 | * @param {number} byteOffset Byte offset of the uploaded blob in the target object. 525 | * @param {number} totalBytes Total byte size of the target object. 526 | * @param {string} sessionId Resumable session ID. 527 | * @param {string} [contentType='application/stream'] Type of content to be used in HTTP headers, for example, "application/json". 528 | * @throws Error when the request fails, for example, due to insufficient rights, or incorrect scopes. 529 | */ 530 | async uploadObjectStreamResumable(bucketKey: string, objectName: string, stream: ReadableStream, chunkBytes: number, byteOffset: number, totalBytes: number, sessionId: string, contentType: string = 'application/stream') { 531 | console.warn('This method is deprecated and will be removed in future versions.'); 532 | const headers = { 533 | 'Authorization': '', 534 | 'Content-Type': contentType, 535 | 'Content-Length': chunkBytes.toString(), 536 | 'Content-Range': `bytes ${byteOffset}-${byteOffset + chunkBytes - 1}/${totalBytes}`, 537 | 'Session-Id': sessionId 538 | } 539 | return this.put(`buckets/${bucketKey}/objects/${encodeURIComponent(objectName)}/resumable`, stream, headers, WriteTokenScopes); 540 | } 541 | 542 | /** 543 | * @deprecated This method of resumable upload is now deprecated and will be removed in future versions. 544 | * Use {@see getUploadUrls} and {@see completeUpload} instead. 545 | * 546 | * Gets status of a resumable upload session 547 | * ({@link https://aps.autodesk.com/en/docs/data/v2/reference/http/buckets-:bucketKey-objects-:objectName-status-:sessionId-GET|docs}). 548 | * @async 549 | * @param {string} bucketKey Bucket key. 550 | * @param {string} objectName Name of uploaded object. 551 | * @param {string} sessionId Resumable session ID. 552 | * @returns {Promise} List of range objects, with each object specifying 'start' and 'end' byte offsets 553 | * of data that has already been uploaded. 554 | * @throws Error when the request fails, for example, due to insufficient rights, or incorrect scopes. 555 | */ 556 | async getResumableUploadStatus(bucketKey: string, objectName: string, sessionId: string): Promise { 557 | console.warn('This method is deprecated and will be removed in future versions.'); 558 | const config: AxiosRequestConfig = { 559 | method: 'GET', 560 | url: `buckets/${bucketKey}/objects/${encodeURIComponent(objectName)}/status/${sessionId}`, 561 | headers: { 'Authorization': '' } 562 | }; 563 | await this.setAuthorization(config, ReadTokenScopes); 564 | const response = await this.fetch(config); 565 | const ranges = response.headers['range'] || ''; 566 | const match = ranges.match(/^bytes=(\d+-\d+(,\d+-\d+)*)$/); 567 | if (match) { 568 | return match[1].split(',').map((str: string) => { 569 | const tokens = str.split('-'); 570 | return { 571 | start: parseInt(tokens[0]), 572 | end: parseInt(tokens[1]) 573 | }; 574 | }); 575 | } else { 576 | throw new Error('Unexpected range format: ' + ranges); 577 | } 578 | } 579 | 580 | /** 581 | * Makes a copy of object under another name within the same bucket 582 | * ({@link https://aps.autodesk.com/en/docs/data/v2/reference/http/buckets-:bucketKey-objects-:objectName-copyto-:newObjectName-PUT|docs}). 583 | * @async 584 | * @param {string} bucket Bucket key. 585 | * @param {string} oldObjectKey Original object key. 586 | * @param {string} newObjectKey New object key. 587 | * @returns {Promise} Details of the new object copy. 588 | * @throws Error when the request fails, for example, due to insufficient rights, or incorrect scopes. 589 | */ 590 | async copyObject(bucket: string, oldObjectKey: string, newObjectKey: string): Promise { 591 | return this.put(`buckets/${bucket}/objects/${encodeURIComponent(oldObjectKey)}/copyto/${encodeURIComponent(newObjectKey)}`, null, {}, WriteTokenScopes); 592 | } 593 | 594 | /** 595 | * Gets details of a specific bucket object 596 | * ({@link https://aps.autodesk.com/en/docs/data/v2/reference/http/buckets-:bucketKey-objects-:objectName-details-GET|docs}). 597 | * @async 598 | * @param {string} bucket Bucket key. 599 | * @param {string} object Object name. 600 | * @returns {Promise} Object description containing 'bucketKey', 'objectKey', 'objectId', 601 | * 'sha1', 'size', 'location', and 'contentType'. 602 | * @throws Error when the request fails, for example, due to insufficient rights, or when an object 603 | * with this name does not exist. 604 | */ 605 | async getObjectDetails(bucket: string, object: string): Promise { 606 | return this.get(`buckets/${bucket}/objects/${encodeURIComponent(object)}/details`, {}, ReadTokenScopes); 607 | } 608 | 609 | /** 610 | * Creates signed URL for specific object 611 | * ({@link https://aps.autodesk.com/en/docs/data/v2/reference/http/buckets-:bucketKey-objects-:objectName-signed-POST|docs}). 612 | * @async 613 | * @param {string} bucketId Bucket key. 614 | * @param {string} objectId Object key. 615 | * @param {string} [access="readwrite"] Signed URL access authorization. 616 | * @param {boolean} [useCdn=true] If true, this will generate a CloudFront URL for the S3 object. 617 | * @returns {Promise} Description of the new signed URL resource. 618 | * @throws Error when the request fails, for example, due to insufficient rights. 619 | */ 620 | async createSignedUrl(bucketId: string, objectId: string, access = 'readwrite', useCdn = true): Promise { 621 | return this.post(`buckets/${bucketId}/objects/${encodeURIComponent(objectId)}/signed?access=${access}&useCdn=${useCdn}`, {}, {}, WriteTokenScopes); 622 | } 623 | 624 | /** 625 | * Deletes object 626 | * ({@link https://aps.autodesk.com/en/docs/data/v2/reference/http/buckets-:bucketKey-objects-:objectName-DELETE|docs}). 627 | * @async 628 | * @param {string} bucketKey Bucket key. 629 | * @param {string} objectName Name of object to delete. 630 | * @throws Error when the request fails, for example, due to insufficient rights, or incorrect scopes. 631 | */ 632 | async deleteObject(bucketKey: string, objectName: string) { 633 | return this.delete(`buckets/${bucketKey}/objects/${encodeURIComponent(objectName)}`, {}, WriteTokenScopes); 634 | } 635 | 636 | /** 637 | * Deletes bucket. 638 | * @async 639 | * @param {string} bucketKey Bucket key. 640 | * @throws Error when the request fails, for example, due to insufficient rights, or incorrect scopes. 641 | */ 642 | async deleteBucket(bucketKey: string) { 643 | return this.delete(`buckets/${bucketKey}`, {}, WriteTokenScopes); 644 | } 645 | } 646 | -------------------------------------------------------------------------------- /src/design-automation.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import FormData from 'form-data'; 3 | import { BaseClient, IAuthOptions, Region } from './common'; 4 | 5 | const CodeScopes = ['code:all']; 6 | 7 | export enum DesignAutomationRegion { 8 | US_WEST = 'us-west', 9 | US_EAST = 'us-east' 10 | } 11 | 12 | export interface IEngineDetail { 13 | productVersion: string; 14 | description: string; 15 | version: number; 16 | id: string; 17 | } 18 | 19 | export interface IAppBundleCommon { 20 | engine: string; 21 | settings?: { [key: string]: any }; 22 | description?: string; 23 | } 24 | 25 | export interface ICreateAppBundleConfig extends IAppBundleCommon { 26 | id: string; 27 | } 28 | 29 | export interface IUpdateAppBundleConfig extends IAppBundleCommon { 30 | } 31 | 32 | export interface IAppBundleDetail extends IAppBundleCommon { 33 | id: string; 34 | version: number; 35 | package: string; 36 | } 37 | 38 | export interface IAppBundleUploadParams extends IAppBundleCommon { 39 | id: string; 40 | version: number; 41 | uploadParameters: { 42 | formData: { [key: string]: string }; 43 | endpointURL: string; 44 | }; 45 | } 46 | 47 | export interface IAlias { 48 | id: string; 49 | version: number; 50 | receiver?: string; 51 | } 52 | 53 | export interface ICodeOnEngineStringSetting { 54 | name: string; 55 | value: string; 56 | isEnvironmentVariable: boolean; 57 | } 58 | 59 | export interface ICodeOnEngineUrlSetting { 60 | name: string; 61 | url: string; 62 | headers?: object; 63 | verb?: string; 64 | } 65 | 66 | export interface IActivityParam { 67 | name: string; 68 | verb?: string; 69 | description?: string; 70 | localName?: string; 71 | required?: boolean; 72 | zip?: boolean; 73 | ondemand?: boolean; 74 | } 75 | 76 | export interface IActivityCommon { 77 | engine: string; 78 | commandLine: string[]; 79 | description?: string; 80 | appbundles?: string[]; 81 | parameters?: { [name: string]: IActivityParam }; 82 | settings?: any; 83 | } 84 | 85 | export interface ICreateActivityConfig extends IActivityCommon { 86 | id: string; 87 | } 88 | 89 | export interface IUpdateActivityConfig extends IActivityCommon { 90 | } 91 | 92 | export interface IActivityDetail extends IActivityCommon { 93 | id: string; 94 | version: number; 95 | } 96 | 97 | export interface IWorkItemConfig { 98 | activityId: string; 99 | arguments?: { [name: string]: IWorkItemParam }; 100 | signatures?: { 101 | activityId?: string; 102 | baseUrls?: { 103 | url: string; 104 | signature: string 105 | }}; 106 | limitProcessingTimeSec?: number 107 | } 108 | 109 | export interface IWorkItemDetail { 110 | id: string; 111 | status: string; 112 | progress: string; 113 | reportUrl: string; 114 | stats: any; 115 | } 116 | 117 | export interface IWorkItemParam { 118 | url: string; 119 | localName?: string; 120 | optional?: boolean; 121 | pathInZip?: string; 122 | headers?: object; 123 | verb?: string; 124 | } 125 | 126 | /** 127 | * Helper class for working with Design Automation 128 | * {@link https://aps.autodesk.com/en/docs/design-automation/v3/developers_guide/aliases-and-ids|aliases and IDs}. 129 | */ 130 | export class DesignAutomationID { 131 | /** 132 | * Parses fully qualified ID. 133 | * @param {string} str Fully qualified ID. 134 | * @returns {DesignAutomationID|null} Parsed ID or null if the format was not correct. 135 | */ 136 | static parse(str: string): DesignAutomationID | null { 137 | const match = str.match(/^([a-zA-Z0-9_]+)\.([a-zA-Z0-9_]+)\+([\$a-zA-Z0-9_]+)$/); 138 | if (match) { 139 | return new DesignAutomationID(match[1], match[2], match[3]); 140 | } else { 141 | return null; 142 | } 143 | } 144 | 145 | /** 146 | * Creates new fully qualified ID. 147 | * @param {string} owner Owner part of the fully qualified ID. Must consist of a-z, A-Z, 0-9 and _ characters only. 148 | * @param {string} id ID part of the fully qualified ID. Must consist of a-z, A-Z, 0-9 and _ characters only. 149 | * @param {string} alias Alias part of the fully qualified ID. Must consist of a-z, A-Z, 0-9 and _ characters only. 150 | */ 151 | constructor(public owner: string, public id: string, public alias: string) { 152 | } 153 | 154 | /** 155 | * Outputs the fully qualified ID in a form expected by Design Automation endpoints. 156 | */ 157 | toString() { 158 | return `${this.owner}.${this.id}+${this.alias}`; 159 | } 160 | } 161 | 162 | /** 163 | * Client providing access to Autodesk Platform Services 164 | * {@link https://aps.autodesk.com/en/docs/design-automation/v3|design automation APIs}. 165 | * @tutorial design-automation 166 | */ 167 | export class DesignAutomationClient extends BaseClient { 168 | /** 169 | * Initializes new client with specific authentication method. 170 | * @param {IAuthOptions} auth Authentication object, 171 | * containing either `client_id` and `client_secret` properties (for 2-legged authentication), 172 | * or a single `token` property (for 2-legged or 3-legged authentication with pre-generated access token). 173 | * @param {string} [host="https://developer.api.autodesk.com"] APS host. 174 | * @param {Region} [_region] @deprecated Will be removed in next major version. 175 | * @param {DesignAutomationRegion} [region] Design Automation specific availability region. 176 | */ 177 | constructor(auth: IAuthOptions, host?: string, _region?: Region, region?: DesignAutomationRegion) { 178 | // TODO: remove _region param 179 | const RootPath = `da/${region || DesignAutomationRegion.US_EAST}/v3`; 180 | super(RootPath, auth, host); 181 | } 182 | 183 | /** 184 | * Resets client to specific authentication method, APS host, and availability region. 185 | * @param {IAuthOptions} [auth] Authentication object, 186 | * containing either `client_id` and `client_secret` properties (for 2-legged authentication), 187 | * or a single `token` property (for 2-legged or 3-legged authentication with pre-generated access token). 188 | * @param {string} [host="https://developer.api.autodesk.com"] APS host. 189 | * @param {Region} [_region] @deprecated Will be removed in next major version. 190 | * @param {DesignAutomationRegion} [region] Design Automation specific availability region. 191 | */ 192 | public reset(auth?: IAuthOptions, host?: string, _region?: Region, region?: DesignAutomationRegion) { 193 | // TODO: remove _region param 194 | this.root = `da/${region || DesignAutomationRegion.US_EAST}/v3`; 195 | super.reset(auth, host); 196 | } 197 | 198 | // Iterates (asynchronously) over pages of paginated results 199 | private async *_pager(endpoint: string, scopes: string[]) { 200 | let response = await this.get(endpoint, {}, scopes); 201 | yield response.data; 202 | 203 | while (response.paginationToken) { 204 | response = await this.get(`${endpoint}?page=${response.paginationToken}`, {}, scopes); 205 | yield response.data; 206 | } 207 | } 208 | 209 | // Collects all pages of paginated results 210 | private async _collect(endpoint: string, scopes: string[]) { 211 | let response = await this.get(endpoint, {}, scopes); 212 | let results = response.data; 213 | 214 | while (response.paginationToken) { 215 | response = await this.get(`${endpoint}?page=${response.paginationToken}`, {}, scopes); 216 | results = results.concat(response.data); 217 | } 218 | return results; 219 | } 220 | 221 | /** 222 | * Gets current nickname for all your Design Automation entities 223 | * ({@link https://aps.autodesk.com/en/docs/design-automation/v3/reference/http/design-automation-forgeapps-id-GET|docs}). 224 | * @async 225 | * @returns {Promise} Current nickname. 226 | */ 227 | async getNickname(): Promise { 228 | return this.get(`forgeapps/me`, {}, CodeScopes); 229 | } 230 | 231 | /** 232 | * Sets new nickname for all your Design Automation entities 233 | * ({@link https://aps.autodesk.com/en/docs/design-automation/v3/reference/http/design-automation-forgeapps-id-PATCH|docs}). 234 | * @async 235 | * @param {string} nickname New nickname. 236 | * @throws Error when the request fails, for example, due to insufficient rights, or incorrect scopes. 237 | */ 238 | async setNickname(nickname: string): Promise { 239 | return this.patch(`forgeapps/me`, { nickname }, { 'Content-Type': 'application/json' }, CodeScopes); 240 | } 241 | 242 | /** 243 | * Removes current nickname for all your Design Automation entities 244 | * ({@link https://aps.autodesk.com/en/docs/design-automation/v3/reference/http/design-automation-forgeapps-id-DELETE|docs}). 245 | * @async 246 | * @throws Error when the request fails, for example, due to insufficient rights, or incorrect scopes. 247 | */ 248 | async deleteNickname(): Promise { 249 | return this.delete(`forgeapps/me`, {}, CodeScopes); 250 | } 251 | 252 | /** 253 | * Iterates over all engines in pages of predefined size 254 | * ({@link https://aps.autodesk.com/en/docs/design-automation/v3/reference/http/engines-GET|docs}). 255 | * @async 256 | * @generator 257 | * @yields {AsyncIterable} List of engine (full) IDs. 258 | * @throws Error when the request fails, for example, due to insufficient rights, or incorrect scopes. 259 | */ 260 | async *iterateEngines(): AsyncIterable { 261 | for await (const engines of this._pager('engines', CodeScopes)) { 262 | yield engines; 263 | } 264 | } 265 | 266 | /** 267 | * Gets a list of all engines 268 | * ({@link https://aps.autodesk.com/en/docs/design-automation/v3/reference/http/engines-GET|docs}). 269 | * @async 270 | * @returns {Promise} List of engine (full) IDs. 271 | * @throws Error when the request fails, for example, due to insufficient rights, or incorrect scopes. 272 | */ 273 | async listEngines(): Promise { 274 | return this._collect('engines', CodeScopes); 275 | } 276 | 277 | /** 278 | * Gets single engine details 279 | * ({@link https://aps.autodesk.com/en/docs/design-automation/v3/reference/http/engines-id-GET|docs}). 280 | * @async 281 | * @param {string} engineId Fully qualified engine ID. 282 | * @returns {Promise} Engine details. 283 | * @throws Error when the request fails, for example, due to insufficient rights, or incorrect scopes. 284 | */ 285 | async getEngine(engineId: string): Promise { 286 | return this.get(`engines/${engineId}`, {}, CodeScopes); 287 | } 288 | 289 | /** 290 | * Iterates over all app bundles in pages of predefined size 291 | * ({@link https://aps.autodesk.com/en/docs/design-automation/v3/reference/http/appbundles-GET|docs}). 292 | * @async 293 | * @generator 294 | * @yields {AsyncIterable} List of appbundle (full) IDs. 295 | * @throws Error when the request fails, for example, due to insufficient rights, or incorrect scopes. 296 | */ 297 | async *iterateAppBundles(): AsyncIterable { 298 | for await (const bundles of this._pager('appbundles', CodeScopes)) { 299 | yield bundles; 300 | } 301 | } 302 | 303 | /** 304 | * Gets a list of all appbundles 305 | * ({@link https://aps.autodesk.com/en/docs/design-automation/v3/reference/http/appbundles-GET|docs}). 306 | * @async 307 | * @returns {Promise} List of appbundle (full) IDs. 308 | * @throws Error when the request fails, for example, due to insufficient rights, or incorrect scopes. 309 | */ 310 | async listAppBundles(): Promise { 311 | return this._collect('appbundles', CodeScopes); 312 | } 313 | 314 | /** 315 | * Gets single appbundle details 316 | * ({@link https://aps.autodesk.com/en/docs/design-automation/v3/reference/http/appbundles-id-GET|docs}). 317 | * @async 318 | * @param {string} bundleId Fully qualified appbundle ID. 319 | * @returns {Promise} Appbundle details. 320 | * @throws Error when the request fails, for example, due to insufficient rights, or incorrect scopes. 321 | */ 322 | async getAppBundle(bundleId: string): Promise { 323 | return this.get(`appbundles/${bundleId}`, {}, CodeScopes); 324 | } 325 | 326 | /** 327 | * Gets single appbundle version details 328 | * ({@link https://aps.autodesk.com/en/docs/design-automation/v3/reference/http/appbundles-id-versions-version-GET|docs}). 329 | * @async 330 | * @param {string} id Short (unqualified) app bundle ID. 331 | * @param {number} version App bundle version. 332 | * @returns {Promise} Appbundle details. 333 | * @throws Error when the request fails, for example, due to insufficient rights, or incorrect scopes. 334 | */ 335 | async getAppBundleVersion(id: string, version: number): Promise { 336 | return this.get(`appbundles/${id}/versions/${version}`, {}, CodeScopes); 337 | } 338 | 339 | /** 340 | * Creates a new app bundle 341 | * ({@link https://aps.autodesk.com/en/docs/design-automation/v3/reference/http/appbundles-POST|docs}). 342 | * @async 343 | * @param {string} id Unique name of the bundle. 344 | * @param {string} engine ID of one of the supported {@link engines}. 345 | * @param {object} [settings] Additional app bundle settings. 346 | * @param {string} [description] App bundle description. 347 | * @returns {Promise} Details of the created app bundle, 348 | * incl. parameters for uploading the actual zip file with app bundle binaries. 349 | * @throws Error when the request fails, for example, due to insufficient rights. 350 | */ 351 | async createAppBundle(id: string, engine: string, settings?: { [key: string]: any }, description?: string): Promise { 352 | const config: ICreateAppBundleConfig = { id, engine }; 353 | if (settings) { 354 | config.settings = settings; 355 | } 356 | if (description) { 357 | config.description = description; 358 | } 359 | return this.post('appbundles', config, {}, CodeScopes); 360 | } 361 | 362 | /** 363 | * Updates an existing app bundle, creating its new version 364 | * ({@link https://aps.autodesk.com/en/docs/design-automation/v3/reference/http/appbundles-id-versions-POST|docs}). 365 | * @async 366 | * @param {string} id ID of the app bundle. 367 | * @param {string} [engine] ID of one of the supported {@link engines}. 368 | * @param {object} [settings] Additional app bundle settings. 369 | * @param {string} [description] App bundle description. 370 | * @returns {Promise} Details of the created app bundle, 371 | * incl. parameters for uploading the actual zip file with app bundle binaries. 372 | * @throws Error when the request fails, for example, due to insufficient rights. 373 | */ 374 | async updateAppBundle(id: string, engine: string, settings?: { [key: string]: any }, description?: string): Promise { 375 | // TODO: tests 376 | const config: IUpdateAppBundleConfig = { engine }; 377 | if (settings) { 378 | config.settings = settings; 379 | } 380 | if (description) { 381 | config.description = description; 382 | } 383 | return this.post(`appbundles/${id}/versions`, config, {}, CodeScopes); 384 | } 385 | 386 | /** 387 | * Uploads zip file with contents of a specific app bundle. 388 | * @async 389 | * @param {IAppBundleUploadParams} appBundleUploadParams App bundle upload parameters 390 | * (returned by {@link createAppBundle} and {@link updateAppBundle}). 391 | * @param {fs.ReadStream} appBundleStream Stream to read the app bundle zip from. 392 | * @returns {Promise} Response from the file submission. 393 | * @example 394 | * const appBundle = await designAutomationClient.createAppBundle('MyAppBundle', 'Autodesk.Inventor+23', 'My App Bundle Description'); 395 | * const appBundleStream = fs.createReadStream('./MyAppBundle.zip'); 396 | * await designAutomationClient.uploadAppBundleArchive(appBundle, appBundleStream); 397 | */ 398 | uploadAppBundleArchive(appBundleUploadParams: IAppBundleUploadParams, appBundleStream: fs.ReadStream): Promise { 399 | const uploadParameters = appBundleUploadParams.uploadParameters.formData; 400 | const form = new FormData(); 401 | form.append('key', uploadParameters['key']); 402 | form.append('policy', uploadParameters['policy']); 403 | form.append('content-type', uploadParameters['content-type']); 404 | form.append('success_action_status', uploadParameters['success_action_status']); 405 | form.append('success_action_redirect', uploadParameters['success_action_redirect']); 406 | form.append('x-amz-signature', uploadParameters['x-amz-signature']); 407 | form.append('x-amz-credential', uploadParameters['x-amz-credential']); 408 | form.append('x-amz-algorithm', uploadParameters['x-amz-algorithm']); 409 | form.append('x-amz-date', uploadParameters['x-amz-date']); 410 | form.append('x-amz-server-side-encryption', uploadParameters['x-amz-server-side-encryption']); 411 | form.append('x-amz-security-token', uploadParameters['x-amz-security-token']); 412 | form.append('file', appBundleStream); 413 | return new Promise(function(resolve, reject) { 414 | form.submit(appBundleUploadParams.uploadParameters.endpointURL, function(err, res) { 415 | if (err) { 416 | reject(err); 417 | } else { 418 | resolve({}); 419 | } 420 | }); 421 | }); 422 | } 423 | 424 | /** 425 | * Iterates over all app bundle aliases in pages of predefined size 426 | * ({@link https://aps.autodesk.com/en/docs/design-automation/v3/reference/http/appbundles-id-aliases-GET|docs}). 427 | * @async 428 | * @generator 429 | * @param {string} name Unique name of the bundle. 430 | * @yields {AsyncIterable} List of appbundle alias objects. 431 | * @throws Error when the request fails, for example, due to insufficient rights, or incorrect scopes. 432 | */ 433 | async *iterateAppBundleAliases(name: string): AsyncIterable { 434 | for await (const aliases of this._pager(`appbundles/${name}/aliases`, CodeScopes)) { 435 | yield aliases; 436 | } 437 | } 438 | 439 | /** 440 | * Gets a list of all appbundle aliases 441 | * ({@link https://aps.autodesk.com/en/docs/design-automation/v3/reference/http/appbundles-id-aliases-GET|docs}). 442 | * @async 443 | * @param {string} name Unique name of the bundle. 444 | * @returns {Promise} List of appbundle alias objects. 445 | * @throws Error when the request fails, for example, due to insufficient rights, or incorrect scopes. 446 | */ 447 | async listAppBundleAliases(name: string): Promise { 448 | return this._collect(`appbundles/${name}/aliases`, CodeScopes); 449 | } 450 | 451 | /** 452 | * Iterates over all app bundle versions in pages of predefined size 453 | * ({@link https://aps.autodesk.com/en/docs/design-automation/v3/reference/http/appbundles-id-versions-GET|docs}). 454 | * @async 455 | * @generator 456 | * @param {string} name Unique name of the bundle. 457 | * @yields {AsyncIterable} List of appbundle version numbers. 458 | * @throws Error when the request fails, for example, due to insufficient rights, or incorrect scopes. 459 | */ 460 | async *iterateAppBundleVersions(name: string): AsyncIterable { 461 | for await (const versions of this._pager(`appbundles/${name}/versions`, CodeScopes)) { 462 | yield versions; 463 | } 464 | } 465 | 466 | /** 467 | * Gets a list of all appbundle versions 468 | * ({@link https://aps.autodesk.com/en/docs/design-automation/v3/reference/http/appbundles-id-versions-GET|docs}). 469 | * @async 470 | * @param {string} name Unique name of the bundle. 471 | * @returns {Promise} List of appbundle version numbers. 472 | * @throws Error when the request fails, for example, due to insufficient rights, or incorrect scopes. 473 | */ 474 | async listAppBundleVersions(name: string): Promise { 475 | return this._collect(`appbundles/${name}/versions`, CodeScopes); 476 | } 477 | 478 | /** 479 | * Creates new alias for an app bundle 480 | * ({@link https://aps.autodesk.com/en/docs/design-automation/v3/reference/http/appbundles-id-aliases-POST/|docs}). 481 | * @async 482 | * @param {string} name Name of the app bundle. 483 | * @param {string} alias Alias name. 484 | * @param {number} version Version of app bundle to link to this alias. 485 | * @param {string} [receiver] Optional ID of another APS application to share this app bundle with. 486 | * @returns {Promise} Details of the created alias. 487 | * @throws Error when the request fails, for example, due to insufficient rights. 488 | */ 489 | async createAppBundleAlias(name: string, alias: string, version: number, receiver?: string): Promise { 490 | // TODO: tests 491 | const config: { id: string; version: number; receiver?: string; } = { id: alias, version: version }; 492 | if (receiver) { 493 | config.receiver = receiver; 494 | } 495 | return this.post(`appbundles/${name}/aliases`, config, {}, CodeScopes); 496 | } 497 | 498 | /** 499 | * Updates existing alias for an app bundle 500 | * ({@link https://aps.autodesk.com/en/docs/design-automation/v3/reference/http/appbundles-id-aliases-aliasId-PATCH/|docs}). 501 | * @async 502 | * @param {string} name Name of the app bundle. 503 | * @param {string} alias Alias name. 504 | * @param {number} version Version of app bundle to link to this alias. 505 | * @param {string} [receiver] Optional ID of another APS application to share this app bundle with. 506 | * @returns {Promise} Details of the updated alias. 507 | * @throws Error when the request fails, for example, due to insufficient rights. 508 | */ 509 | async updateAppBundleAlias(name: string, alias: string, version: number, receiver?: string): Promise { 510 | // TODO: tests 511 | const config: { version: number; receiver?: string; } = { version: version }; 512 | if (receiver) { 513 | config.receiver = receiver; 514 | } 515 | return this.patch(`appbundles/${name}/aliases/${alias}`, config, {}, CodeScopes); 516 | } 517 | 518 | /** 519 | * Deletes app bundle and all its versions and aliases 520 | * ({@link https://aps.autodesk.com/en/docs/design-automation/v3/reference/http/appbundles-id-DELETE|docs}). 521 | * @async 522 | * @param {string} shortId Short (unqualified) app bundle ID. 523 | */ 524 | async deleteAppBundle(shortId: string) { 525 | return this.delete(`appbundles/${shortId}`, {}, CodeScopes); 526 | } 527 | 528 | /** 529 | * Deletes app bundle alias 530 | * ({@link https://aps.autodesk.com/en/docs/design-automation/v3/reference/http/appbundles-id-aliases-aliasId-DELETE|docs}). 531 | * @async 532 | * @param {string} shortId Short (unqualified) app bundle ID. 533 | * @param {string} alias App bundle alias. 534 | */ 535 | async deleteAppBundleAlias(shortId: string, alias: string) { 536 | return this.delete(`appbundles/${shortId}/aliases/${alias}`, {}, CodeScopes); 537 | } 538 | 539 | /** 540 | * Deletes app bundle version 541 | * ({@link https://aps.autodesk.com/en/docs/design-automation/v3/reference/http/appbundles-id-versions-version-DELETE|docs}). 542 | * @async 543 | * @param {string} shortId Short (unqualified) app bundle ID. 544 | * @param {number} version App bundle version. 545 | */ 546 | async deleteAppBundleVersion(shortId: string, version: number) { 547 | return this.delete(`appbundles/${shortId}/versions/${version}`, {}, CodeScopes); 548 | } 549 | 550 | /** 551 | * Iterates over all activities in pages of predefined size 552 | * ({@link https://aps.autodesk.com/en/docs/design-automation/v3/reference/http/activities-GET|docs}). 553 | * @async 554 | * @generator 555 | * @yields {AsyncIterable} List of activity (full) IDs. 556 | * @throws Error when the request fails, for example, due to insufficient rights, or incorrect scopes. 557 | */ 558 | async *iterateActivities(): AsyncIterable { 559 | for await (const activities of this._pager('activities', CodeScopes)) { 560 | yield activities; 561 | } 562 | } 563 | 564 | /** 565 | * Gets a list of all activities 566 | * ({@link https://aps.autodesk.com/en/docs/design-automation/v3/reference/http/activities-GET|docs}). 567 | * @async 568 | * @returns {Promise} List of activity (full) IDs. 569 | * @throws Error when the request fails, for example, due to insufficient rights, or incorrect scopes. 570 | */ 571 | async listActivities(): Promise { 572 | return this._collect('activities', CodeScopes); 573 | } 574 | 575 | /** 576 | * Gets single activity details 577 | * ({@link https://aps.autodesk.com/en/docs/design-automation/v3/reference/http/activities-id-GET|docs}). 578 | * @async 579 | * @param {string} activityId Fully qualified activity ID. 580 | * @returns {Promise} Activity details. 581 | * @throws Error when the request fails, for example, due to insufficient rights, or incorrect scopes. 582 | */ 583 | async getActivity(activityId: string): Promise { 584 | return this.get(`activities/${activityId}`, {}, CodeScopes); 585 | } 586 | 587 | /** 588 | * Gets single activity version details 589 | * ({@link https://aps.autodesk.com/en/docs/design-automation/v3/reference/http/activities-id-versions-version-GET|docs}). 590 | * @async 591 | * @param {string} id Short (unqualified) activity ID. 592 | * @param {number} version Activity version. 593 | * @returns {Promise} Activity details. 594 | * @throws Error when the request fails, for example, due to insufficient rights, or incorrect scopes. 595 | */ 596 | async getActivityVersion(id: string, version: number): Promise { 597 | return this.get(`activities/${id}/versions/${version}`, {}, CodeScopes); 598 | } 599 | 600 | /** 601 | * Creates new activity 602 | * ({@link https://aps.autodesk.com/en/docs/design-automation/v3/reference/http/activities-POST|docs}). 603 | * @async 604 | * @param {string} id New activity ID. 605 | * @param {string} engine ID of one of the supported {@link engines}. 606 | * @param {string | string[]} commands One or more CLI commands to be executed within the activity. 607 | * @param {string | string[]} [appBundleIDs] Fully qualified IDs of zero or more app bundles used by the activity. 608 | * @param {{ [name: string]: IActivityParam }} [parameters] Input/output parameter descriptors. 609 | * @param {{ [key: string]: any }} [settings] Additional activity settings. 610 | * @param {string} [description] Activity description. 611 | * @returns {Promise} Details of created activity. 612 | */ 613 | async createActivity(id: string, engine: string, commands: string | string[], appBundleIDs?: string | string[], 614 | parameters?: { [key: string]: IActivityParam }, 615 | settings?: { [key: string]: (ICodeOnEngineStringSetting | ICodeOnEngineUrlSetting) }, 616 | description?: string): Promise { 617 | // TODO: tests 618 | if (!this.auth) { 619 | throw new Error('Cannot create activity without client ID.'); 620 | } 621 | const config: ICreateActivityConfig = { 622 | id, 623 | engine, 624 | commandLine: Array.isArray(commands) ? commands : [commands] 625 | }; 626 | if (appBundleIDs) { 627 | config.appbundles = Array.isArray(appBundleIDs) ? appBundleIDs : [appBundleIDs]; 628 | } 629 | if (parameters) { 630 | config.parameters = parameters; 631 | } 632 | if (settings) { 633 | config.settings = settings; 634 | } 635 | if (description) { 636 | config.description = description; 637 | } 638 | return this.post('activities', config, {}, CodeScopes); 639 | } 640 | 641 | /** 642 | * Updates existing activity, creating its new version 643 | * ({@link https://aps.autodesk.com/en/docs/design-automation/v3/reference/http/activities-id-versions-POST|docs}). 644 | * @async 645 | * @param {string} id New activity ID. 646 | * @param {string} engine ID of one of the supported {@link engines}. 647 | * @param {string | string[]} commands One or more CLI commands to be executed within the activity. 648 | * @param {string | string[]} [appBundleIDs] Fully qualified IDs of zero or more app bundles used by the activity. 649 | * @param {{ [name: string]: IActivityParam }} [parameters] Input/output parameter descriptors. 650 | * @param {{ [key: string]: any }} [settings] Additional activity settings. 651 | * @param {string} [description] Activity description. 652 | * @returns {Promise} Details of created activity. 653 | */ 654 | async updateActivity(id: string, engine: string, commands: string | string[], appBundleIDs?: string | string[], 655 | parameters?: { [key: string]: IActivityParam }, settings?: { [key: string]: any }, description?: string): Promise { 656 | // TODO: tests 657 | if (!this.auth) { 658 | throw new Error('Cannot create activity without client ID.'); 659 | } 660 | const config: IUpdateActivityConfig = { 661 | engine, 662 | commandLine: Array.isArray(commands) ? commands : [commands] 663 | }; 664 | if (appBundleIDs) { 665 | config.appbundles = Array.isArray(appBundleIDs) ? appBundleIDs : [appBundleIDs]; 666 | } 667 | if (parameters) { 668 | config.parameters = parameters; 669 | } 670 | if (settings) { 671 | config.settings = settings; 672 | } 673 | if (description) { 674 | config.description = description; 675 | } 676 | return this.post(`activities/${id}/versions`, config, {}, CodeScopes); 677 | } 678 | 679 | /** 680 | * Iterates over all activity aliases in pages of predefined size 681 | * ({@link https://aps.autodesk.com/en/docs/design-automation/v3/reference/http/activities-id-aliases-GET|docs}). 682 | * @async 683 | * @generator 684 | * @param {string} name Unique name of activity. 685 | * @yields {AsyncIterable} List of activity alias objects. 686 | * @throws Error when the request fails, for example, due to insufficient rights, or incorrect scopes. 687 | */ 688 | async *iterateActivityAliases(name: string): AsyncIterable { 689 | for await (const aliases of this._pager(`activities/${name}/aliases`, CodeScopes)) { 690 | yield aliases; 691 | } 692 | } 693 | 694 | /** 695 | * Gets a list of all activity aliases 696 | * ({@link https://aps.autodesk.com/en/docs/design-automation/v3/reference/http/activities-id-aliases-GET|docs}). 697 | * @async 698 | * @param {string} name Unique name of activity. 699 | * @returns {Promise} List of activity alias objects. 700 | * @throws Error when the request fails, for example, due to insufficient rights, or incorrect scopes. 701 | */ 702 | async listActivityAliases(name: string): Promise { 703 | return this._collect(`activities/${name}/aliases`, CodeScopes); 704 | } 705 | 706 | /** 707 | * Iterates over all activity versions in pages of predefined size 708 | * ({@link https://aps.autodesk.com/en/docs/design-automation/v3/reference/http/activities-id-versions-GET|docs}). 709 | * @async 710 | * @generator 711 | * @param {string} name Unique name of activity. 712 | * @yields {AsyncIterable} List of activity versions. 713 | * @throws Error when the request fails, for example, due to insufficient rights, or incorrect scopes. 714 | */ 715 | async *iterateActivityVersions(name: string): AsyncIterable { 716 | for await (const versions of this._pager(`activities/${name}/versions`, CodeScopes)) { 717 | yield versions; 718 | } 719 | } 720 | 721 | /** 722 | * Gets a list of all activity versions 723 | * ({@link https://aps.autodesk.com/en/docs/design-automation/v3/reference/http/activities-id-versions-GET|docs}). 724 | * @async 725 | * @param {string} name Unique name of activity. 726 | * @returns {Promise} List of activity versions. 727 | * @throws Error when the request fails, for example, due to insufficient rights, or incorrect scopes. 728 | */ 729 | async listActivityVersions(name: string): Promise { 730 | return this._collect(`activities/${name}/versions`, CodeScopes); 731 | } 732 | 733 | /** 734 | * Creates new alias for an activity 735 | * ({@link https://aps.autodesk.com/en/docs/design-automation/v3/reference/http/activities-id-aliases-POST|docs}). 736 | * @async 737 | * @param {string} id Activity ID. 738 | * @param {string} alias New alias name. 739 | * @param {number} version Activity version to link to this alias. 740 | * @param {string} [receiver] Optional ID of another APS application to share this activity with. 741 | * @returns {Promise} Details of created alias. 742 | */ 743 | async createActivityAlias(id: string, alias: string, version: number, receiver?: string): Promise { 744 | // TODO: tests 745 | const config: { id: string; version: number; receiver?: string; } = { id: alias, version: version }; 746 | if (receiver) { 747 | config.receiver = receiver; 748 | } 749 | return this.post(`activities/${id}/aliases`, config, {}, CodeScopes); 750 | } 751 | 752 | /** 753 | * Updates existing alias for an activity 754 | * ({@link https://aps.autodesk.com/en/docs/design-automation/v3/reference/http/activities-id-aliases-aliasId-PATCH|docs}). 755 | * @async 756 | * @param {string} id Activity ID. 757 | * @param {string} alias Activity alias. 758 | * @param {number} version Activity version to link to this alias. 759 | * @param {string} [receiver] Optional ID of another APS application to share this activity with. 760 | * @returns {Promise} Details of updated alias. 761 | */ 762 | async updateActivityAlias(id: string, alias: string, version: number, receiver?: string): Promise { 763 | // TODO: tests 764 | const config: { version: number; receiver?: string; } = { version: version }; 765 | if (receiver) { 766 | config.receiver = receiver; 767 | } 768 | return this.patch(`activities/${id}/aliases/${alias}`, config, {}, CodeScopes); 769 | } 770 | 771 | /** 772 | * Deletes activity and all its versions and aliases 773 | * ({@link https://aps.autodesk.com/en/docs/design-automation/v3/reference/http/activities-id-DELETE|docs}). 774 | * @async 775 | * @param {string} shortId Short (unqualified) activity ID. 776 | */ 777 | async deleteActivity(shortId: string) { 778 | return this.delete(`activities/${shortId}`, {}, CodeScopes); 779 | } 780 | 781 | /** 782 | * Deletes activity alias 783 | * ({@link https://aps.autodesk.com/en/docs/design-automation/v3/reference/http/activities-id-aliases-aliasId-DELETE|docs}). 784 | * @async 785 | * @param {string} shortId Short (unqualified) activity ID. 786 | * @param {string} alias Activity alias. 787 | */ 788 | async deleteActivityAlias(shortId: string, alias: string) { 789 | return this.delete(`activities/${shortId}/aliases/${alias}`, {}, CodeScopes); 790 | } 791 | 792 | /** 793 | * Deletes activity version 794 | * ({@link https://aps.autodesk.com/en/docs/design-automation/v3/reference/http/activities-id-versions-version-DELETE|docs}). 795 | * @async 796 | * @param {string} shortId Short (unqualified) activity ID. 797 | * @param {number} version Activity version. 798 | */ 799 | async deleteActivityVersion(shortId: string, version: number) { 800 | return this.delete(`activities/${shortId}/versions/${version}`, {}, CodeScopes); 801 | } 802 | 803 | /** 804 | * Gets details of a specific work item 805 | * ({@link https://aps.autodesk.com/en/docs/design-automation/v3/reference/http/workitems-id-GET|docs}). 806 | * @async 807 | * @param {string} id Work item ID. 808 | * @returns {Promise} Work item details. 809 | * @throws Error when the request fails, for example, due to insufficient rights. 810 | */ 811 | async getWorkItem(id: string): Promise { 812 | return this.get(`workitems/${id}`, {}, CodeScopes); 813 | } 814 | 815 | /** 816 | * Creates new work item 817 | * ({@link https://aps.autodesk.com/en/docs/design-automation/v3/reference/http/workitems-POST|docs}). 818 | * @async 819 | * @param {string} activityId Activity ID. 820 | * @param {{ [name: string]: IWorkItemParam }} [args] Arguments to pass in as activity parameters. 821 | * @param {{ activityId?: string; baseUrls?: { url: string; signature: string } }} signatures Signatures. 822 | * @param {number} limitProcessingTimeSec limit of max processing time in seconds. 823 | */ 824 | async createWorkItem(activityId: string, args?: { [name: string]: IWorkItemParam }, 825 | signatures?: { 826 | activityId?: string; 827 | baseUrls?: { 828 | url: string; 829 | signature: string; 830 | } 831 | }, 832 | limitProcessingTimeSec?: number) { 833 | // TODO: tests 834 | const config: IWorkItemConfig = { 835 | activityId: activityId 836 | }; 837 | if (args) { 838 | config.arguments = args; 839 | } 840 | if (signatures) { 841 | config.signatures = signatures; 842 | } 843 | if (limitProcessingTimeSec) { 844 | config.limitProcessingTimeSec = limitProcessingTimeSec; 845 | } 846 | return this.post('workitems', config, {}, CodeScopes); 847 | } 848 | 849 | /** 850 | * Cancels work item, removing it from waiting queue or cancelling a running job 851 | * ({@link https://aps.autodesk.com/en/docs/design-automation/v3/reference/http/workitems-id-DELETE|docs}). 852 | * @async 853 | * @param {string} id Work item ID. 854 | */ 855 | async deleteWorkItem(id: string) { 856 | return this.delete(`workitems/${id}`, {}, CodeScopes); 857 | } 858 | } 859 | --------------------------------------------------------------------------------