├── schema └── .DS_Store ├── .gitignore ├── .npmignore ├── examples ├── sql │ ├── mask.sql │ ├── cfa.sql │ ├── max_busy.sql │ ├── README.md │ ├── sqlQuery.js │ └── unassignedDn.sql ├── copy_sip_trunk │ ├── README.md │ └── copySipTrunk.js ├── getTagsForOperation.js ├── templates │ ├── update_phone_update_line │ │ ├── lineTemplate.json │ │ ├── phoneTemplate.json │ │ └── updatePhoneUpdateLine.js │ ├── add_phone_update_line │ │ ├── lineTemplate.json │ │ ├── addPhoneUpdateLine.js │ │ └── phoneTemplate.json │ ├── README.md │ └── save_search_as_template │ │ └── saveSearch.js ├── typescript │ ├── README.md │ └── example.ts ├── copy_phone │ └── copyPhone.js └── change_phone_model │ └── changePhoneModel.js ├── src ├── types │ └── strong-soap.d.ts └── index.ts ├── test ├── verify.js ├── wsdl.js ├── resetPhoneTest.js ├── listCss.js ├── updateLineTest.js └── tests.js ├── tsconfig.json ├── FUTURE_IMPROVEMENTS.md ├── MIT-LICENSE.txt ├── CLAUDE.md ├── package.json ├── README.md └── index.js /schema/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sieteunoseis/cisco-axl/HEAD/schema/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.DS_Store 2 | /node_modules 3 | .env 4 | /env 5 | /dist 6 | CLAUDE.md 7 | FUTURE_IMPROVEMENTS.md 8 | **/.claude/settings.local.json 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Development files 2 | /.DS_Store 3 | /node_modules 4 | .env 5 | /env 6 | /test 7 | /examples 8 | CLAUDE.md 9 | FUTURE_IMPROVEMENTS.md 10 | 11 | # Do not ignore dist directory for npm 12 | !/dist -------------------------------------------------------------------------------- /examples/sql/mask.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | LIMIT 10 dnorpattern, 3 | calledpartytransformationmask, 4 | callingpartytransformationmask, 5 | callingpartyprefixdigits, 6 | prefixdigitsout 7 | FROM numplan -------------------------------------------------------------------------------- /examples/copy_sip_trunk/README.md: -------------------------------------------------------------------------------- 1 | # Copy SIP Trunk 2 | 3 | Use **cisco-axl** to copy a SIP trunk. Cisco doesn't provide a way to super copy a SIP trunk and add a new one. This script will pull the values for an existing trunk and then allow you to make a few updates and reinsert the new trunk. 4 | -------------------------------------------------------------------------------- /examples/sql/cfa.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | LIMIT 10 dnorpattern, 3 | cfhrdn, 4 | cfhrintdn, 5 | cfbdestination, 6 | cfbintdestination, 7 | cfnadestination, 8 | cfnaintdestination, 9 | cfurdestination, 10 | cfurintdestination, 11 | devicefailuredn, 12 | pffdestination, 13 | pffintdestination 14 | FROM numplan -------------------------------------------------------------------------------- /examples/getTagsForOperation.js: -------------------------------------------------------------------------------- 1 | const axlService = require("../index"); 2 | 3 | // Set up new AXL service 4 | let service = new axlService( 5 | "10.10.20.1", 6 | "administrator", 7 | "ciscopsdt", 8 | "14.0" 9 | ); 10 | 11 | (async () => { 12 | var operation = "addSipTrunk"; 13 | var tags = await service.getOperationTags(operation); 14 | console.log(tags); 15 | })(); 16 | -------------------------------------------------------------------------------- /examples/templates/update_phone_update_line/lineTemplate.json: -------------------------------------------------------------------------------- 1 | { 2 | "pattern": "%%_extension_%%", 3 | "routePartitionName": "", 4 | "alertingName": "%%_firstName_%% %%_lastName_%%", 5 | "asciiAlertingName": "%%_firstName_%% %%_lastName_%%", 6 | "description": "%%_firstName_%% %%_lastName_%%", 7 | "_data": { 8 | "extension": "", 9 | "firstName": "", 10 | "lastName": "" 11 | } 12 | } -------------------------------------------------------------------------------- /examples/templates/add_phone_update_line/lineTemplate.json: -------------------------------------------------------------------------------- 1 | { 2 | "pattern": "%%_extension_%%", 3 | "routePartitionName": "", 4 | "alertingName": "%%_firstName_%% %%_lastName_%%", 5 | "asciiAlertingName": "%%_firstName_%% %%_lastName_%%", 6 | "description": "%%_firstName_%% %%_lastName_%%", 7 | "_data": { 8 | "extension": "2001", 9 | "firstName": "Tom", 10 | "lastName": "Smith" 11 | } 12 | } -------------------------------------------------------------------------------- /examples/sql/max_busy.sql: -------------------------------------------------------------------------------- 1 | SELECT NAME, 2 | param 3 | FROM typemodel AS model, 4 | productsupportsfeature AS p 5 | WHERE p.tkmodel = model.enum 6 | AND p.tksupportsfeature = 7 | (SELECT enum 8 | FROM typesupportsfeature 9 | WHERE NAME = 'Multiple Call Display') 10 | AND model.tkclass = 11 | (SELECT enum 12 | FROM typeclass 13 | WHERE NAME = 'Phone') 14 | AND name='Cisco Dual Mode for Android' -------------------------------------------------------------------------------- /src/types/strong-soap.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'strong-soap' { 2 | export namespace soap { 3 | export class WSDL { 4 | static open(wsdlUri: string, options: any, callback: (err: any, wsdl: any) => void): void; 5 | } 6 | 7 | export function createClient(wsdlPath: string, options: any, callback: (err: any, client: any) => void): void; 8 | 9 | export class BasicAuthSecurity { 10 | constructor(username: string, password: string); 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /test/verify.js: -------------------------------------------------------------------------------- 1 | const axlService = require('../dist/index.js'); 2 | 3 | console.log('Module loaded successfully:', typeof axlService === 'function'); 4 | console.log('Module name:', axlService.name); 5 | 6 | // Create an instance with dummy values just to verify constructor works 7 | try { 8 | const service = new axlService('localhost', 'user', 'pass', '15.0'); 9 | console.log('Instance created successfully'); 10 | } catch (error) { 11 | console.error('Error creating instance:', error); 12 | } -------------------------------------------------------------------------------- /examples/sql/README.md: -------------------------------------------------------------------------------- 1 | # SQL Queries 2 | 3 | Use **cisco-axl** to save SQL queries and execute whenever needed. sqlQuery.js will read in an ".sql" file and run the query for you. Just export the results to a csv or use in another operation. 4 | 5 | The **fs.readFileSync** package will allow a more readable format to be read in and still able executed with the **executeSQLQuery** operation. This makes it easy to store useful queries and script them to run. 6 | 7 | I've included examples of some useful queries. 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "declaration": true, 7 | "outDir": "./dist", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "skipLibCheck": true, 12 | "sourceMap": true, 13 | "typeRoots": [ 14 | "./node_modules/@types", 15 | "./src/types" 16 | ] 17 | }, 18 | "include": ["src/**/*"], 19 | "exclude": ["node_modules", "**/*.test.ts"] 20 | } -------------------------------------------------------------------------------- /examples/templates/README.md: -------------------------------------------------------------------------------- 1 | # Templates 2 | 3 | Using json-variables you can use JSON files as templates along with variables to automate AXL operations. 4 | 5 | ## Update Phone and Line 6 | 7 | Every wanted to change an exsiting phone from one user to another user? This script will help you do that as well as updating all the display, line text labels, etc for the new user. 8 | 9 | ## Add Phone and Update Line 10 | 11 | This script will add a new phone and line. Once finished it will go back and update the line. AXL doesn't allow setting alertingName, asciiAlertingName and description while adding a line via the addPhone operation. This would be similar to using BAT to add phones. 12 | -------------------------------------------------------------------------------- /examples/templates/update_phone_update_line/phoneTemplate.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"%%_deviceName_%%", 3 | "description":"%%_lastName_%%, %%_firstName_%% Extension %%_extension_%%", 4 | "ownerUserName": "%%_userid_%%", 5 | "lines":{ 6 | "line":[ 7 | { 8 | "index":"1", 9 | "dirn":{ 10 | "pattern":"%%_extension_%%" 11 | }, 12 | "label":"%%_lastName_%% - %%_extension_%%", 13 | "display":"%%_lastName_%%, %%_firstName_%%", 14 | "displayAscii":"%%_lastName_%%, %%_firstName_%%", 15 | "associatedEndusers":{ 16 | "enduser":[ 17 | { 18 | "userId":"%%_userid_%%" 19 | } 20 | ] 21 | } 22 | }] 23 | }, 24 | "_data": { 25 | "deviceName":"", 26 | "extension": "", 27 | "firstName": "", 28 | "lastName": "", 29 | "userid":"" 30 | } 31 | } -------------------------------------------------------------------------------- /examples/typescript/README.md: -------------------------------------------------------------------------------- 1 | # TypeScript Example 2 | 3 | This example demonstrates how to use the cisco-axl library with TypeScript. 4 | 5 | ## Prerequisites 6 | 7 | - Node.js v16.15.0 or later 8 | - TypeScript installed globally: `npm install -g typescript` 9 | 10 | ## Running the Example 11 | 12 | 1. Build the project first: 13 | ``` 14 | npm run build 15 | ``` 16 | 17 | 2. Update the example.ts file with your CUCM server details: 18 | ```typescript 19 | const service = new axlService( 20 | "your-cucm-hostname", 21 | "username", 22 | "password", 23 | "14.0" // CUCM version 24 | ); 25 | ``` 26 | 27 | 3. Run the example: 28 | ``` 29 | npx ts-node example.ts 30 | ``` 31 | 32 | ## Features Demonstrated 33 | 34 | - TypeScript type definitions 35 | - Async/await syntax with proper error handling 36 | - Adding a route partition 37 | - Listing route partitions 38 | 39 | ## TypeScript Benefits 40 | 41 | - Static type checking 42 | - Better IDE autocompletion 43 | - Improved documentation with JSDoc comments 44 | - More maintainable codebase -------------------------------------------------------------------------------- /FUTURE_IMPROVEMENTS.md: -------------------------------------------------------------------------------- 1 | # Future Improvements for cisco-axl 2 | 3 | ## Already Implemented 4 | - ✅ TypeScript support with type definitions 5 | 6 | ## Planned Improvements 7 | 8 | ### Error Handling 9 | - Add more descriptive error messages 10 | - Implement custom error classes for different types of failures 11 | - Add retry mechanism for transient errors 12 | - Better validation of input parameters 13 | 14 | ### Testing 15 | - Implement unit tests with Jest/Mocha 16 | - Add integration tests against mock CUCM server 17 | - Set up CI/CD pipeline for automated testing 18 | 19 | ### Documentation 20 | - Generate API documentation from JSDoc comments 21 | - Add more examples for common use cases 22 | - Create a comprehensive wiki 23 | 24 | ### Performance 25 | - Add caching mechanism for frequently used operations 26 | - Implement connection pooling 27 | - Add batch operation support for multiple requests 28 | 29 | ### Features 30 | - Add logging options and levels 31 | - Create higher-level abstraction for common operations 32 | - Implement a CLI tool for quick testing 33 | - Add easy configuration for environment variables -------------------------------------------------------------------------------- /MIT-LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2023 Scott Chacon and others 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | # Cisco AXL Library Dev Guidelines 2 | 3 | ## Commands 4 | - Build TypeScript: `npm run build` 5 | - Type Check: `npm run type-check` 6 | - Run TS Example: `npm run ts-example` 7 | - Test: `NODE_OPTIONS=--experimental-vm-modules NODE_NO_WARNINGS=1 NODE_TLS_REJECT_UNAUTHORIZED=0 NODE_ENV=test node ./test/tests.js` 8 | - Test WSDL: `NODE_OPTIONS=--experimental-vm-modules NODE_NO_WARNINGS=1 NODE_TLS_REJECT_UNAUTHORIZED=0 NODE_ENV=staging node ./test/wsdl.js` 9 | - Dev environment: `NODE_OPTIONS=--experimental-vm-modules NODE_NO_WARNINGS=1 NODE_TLS_REJECT_UNAUTHORIZED=0 NODE_ENV=development node ./test/tests.js` 10 | 11 | ## Code Style 12 | - Use camelCase for variable and function names 13 | - Class names should be PascalCase 14 | - Use meaningful function and variable names 15 | - Include JSDoc style comments for classes and methods 16 | - Use Promises for async operations, with proper error handling via try/catch or .catch() 17 | - Use const for variables that won't be reassigned 18 | - Handle errors with proper rejection in Promises 19 | - Use ES6+ features (arrow functions, template literals, etc.) 20 | - Format JSON output for readability 21 | - Keep methods focused on single responsibility 22 | - Create helper functions for repeated operations -------------------------------------------------------------------------------- /examples/sql/sqlQuery.js: -------------------------------------------------------------------------------- 1 | const axlService = require("../../index"); // Change this to require("cisco-axl") when using outside this package 2 | var fs = require('fs'); 3 | var path = require('path'); 4 | 5 | /* 6 | Example of how to send SQL queries to CUCM via AXL. We can store our queries in ".sql" files, then we can read them in before executing. 7 | 8 | Note: axlService is Promised based, so we using a nested promise. We wait for the first promise to be fufilled before calling the nested one. 9 | */ 10 | 11 | // Set up new AXL service 12 | let service = new axlService( 13 | "10.10.20.1", 14 | "administrator", 15 | "ciscopsdt", 16 | "14.0" 17 | ); 18 | 19 | (async () => { 20 | // First we'll get the params needed to call executeSQLQuery 21 | var operation = "executeSQLQuery"; 22 | var tags = await service.getOperationTags(operation); 23 | console.log(tags); 24 | 25 | // Next we'll read in our SQL file and update tags with the query 26 | let sql = fs.readFileSync(path.join(__dirname, 'mask.sql'), "utf8"); 27 | tags.sql = sql; 28 | console.log(tags); 29 | 30 | // Lastly let's execute the query on server 31 | await service 32 | .executeOperation(operation, tags) 33 | .then((results) => { 34 | console.log(results); 35 | }) 36 | .catch((error) => { 37 | console.log(error); 38 | }); 39 | })(); -------------------------------------------------------------------------------- /examples/sql/unassignedDn.sql: -------------------------------------------------------------------------------- 1 | SELECT np.dnorpattern AS NUMBER, 2 | np.description AS DESCRIPTION, 3 | cfd.cfavoicemailenabled AS CALL_FORWARD_ALL_VOICEMAIL_ENABLED, 4 | cfd.cfadestination AS CALL_FORWARD_ALL_DESTINATION, 5 | cfbvoicemailenabled AS BUSY_VOICEMAIL_ENABLED, 6 | cfbdestination AS BUSY_EXTERNAL_DESTINTATION, 7 | cfbintvoicemailenabled AS BUSY_INTERNAL_VOICEMAIL_ENABLED, 8 | cfbintdestination AS BUSY_INTERNAL_DESTINATION, 9 | cfnavoicemailenabled AS NOANSWER_VOICEMAIL_ENABLED, 10 | cfnadestination AS NO_ANSWERD_ESTINATION, 11 | cfnaintvoicemailenabled AS NO_ANSWER_INTERNAL_VOICEMAIL_ENABLED, 12 | cfnaintdestination AS NO_ANSWER_INTERNAL_DESTINATION, 13 | rpt.name AS PARTITION 14 | FROM numplan np 15 | INNER JOIN typepatternusage tpu 16 | ON np.tkpatternusage = tpu.enum 17 | LEFT OUTER JOIN callforwarddynamic AS cfd 18 | ON cfd.fknumplan = np.pkid 19 | LEFT OUTER JOIN devicenumplanmap dnmp 20 | ON dnmp.fknumplan = np.pkid 21 | LEFT OUTER JOIN routepartition rpt 22 | ON rpt.pkid = np.fkroutepartition 23 | WHERE tpu.NAME = 'Device' 24 | AND dnmp.pkid IS NULL 25 | ORDER BY dnorpattern ASC -------------------------------------------------------------------------------- /test/wsdl.js: -------------------------------------------------------------------------------- 1 | var soap = require("strong-soap").soap; 2 | var WSDL = soap.WSDL; 3 | 4 | var method = "applyPhone"; 5 | 6 | const wsdlOptions = { 7 | attributesKey: 'attributes', 8 | valueKey: 'value', 9 | xmlKey: 'xml', 10 | }; 11 | 12 | WSDL.open(`./schema/15.0/AXLAPI.wsdl`, wsdlOptions, function (err, wsdl) { 13 | if (err) { 14 | console.log(err); 15 | } 16 | 17 | var operation = wsdl.definitions.bindings.AXLAPIBinding.operations[method]; 18 | // console.log("operation", operation); 19 | var schemas = wsdl.definitions; 20 | // console.log("schemas", schemas); 21 | var schema = wsdl.definitions.schemas['http://www.cisco.com/AXL/API/15.0']; 22 | // console.log("schema", schema); 23 | var operName = operation.$name; 24 | // console.log("operName", operName); 25 | var part = wsdl.definitions.messages.AXLError.parts; 26 | // var complexType = schema.complexTypes[method]; 27 | var operationDesc = operation.describe(wsdl); 28 | console.log("operationDesc", operationDesc); 29 | var requestElements = operationDesc.input.body.elements[0].elements; 30 | 31 | operationDesc.input.body.elements.map((object) => { 32 | console.log("object", object); 33 | var operMatch = new RegExp(object.qname.name, "i"); 34 | if (operName.match(operMatch)) { 35 | nestedObj(object); 36 | } 37 | }); 38 | }); 39 | 40 | const nestedObj = (object) => { 41 | object.elements.map((object) => { 42 | if (object.qname.name === "name"){ 43 | console.log(object.qname.name); 44 | console.log(object); 45 | } 46 | }); 47 | }; 48 | -------------------------------------------------------------------------------- /examples/copy_phone/copyPhone.js: -------------------------------------------------------------------------------- 1 | const axlService = require("../../index"); // Change this to require("cisco-axl") when using outside this package 2 | 3 | /* 4 | Example of how to copy a phone. This is similar to the Super Copy function in CUCM. Just an example of how to do it via AXL. 5 | 6 | Note: axlService is Promised based, so we using a nested promise. We wait for the first promise to be fufilled before calling the nested one. 7 | */ 8 | 9 | // Set up new AXL service 10 | let service = new axlService( 11 | "10.10.20.1", 12 | "administrator", 13 | "ciscopsdt", 14 | "14.0" 15 | ); 16 | 17 | (async () => { 18 | var opts = { 19 | clean: true, 20 | removeAttributes: false, 21 | dataContainerIdentifierTails: "_data", 22 | }; 23 | 24 | var operation = "getPhone"; 25 | var tags = await service.getOperationTags(operation); 26 | tags.name = "CSFUSER001"; 27 | 28 | var returnPhoneTags = await service.executeOperation(operation, tags, opts); 29 | 30 | operation = "addPhone"; 31 | returnPhoneTags.phone.name = "CSFWORDENJ"; 32 | returnPhoneTags.phone.description = "Test phone added via AXL"; 33 | // Confidential Access Mode is returned from CUCM via AXL as undefined if not set on the phone we are copying. 34 | // We either need to set it to '' or delete it complete. Since we're not using this feature, let's delete it from our JSON. 35 | delete returnPhoneTags.phone.confidentialAccess; 36 | 37 | await service 38 | .executeOperation(operation, returnPhoneTags) 39 | .then((results) => { 40 | console.log(results); 41 | }) 42 | .catch((error) => { 43 | console.log(error); 44 | }); 45 | })(); 46 | -------------------------------------------------------------------------------- /examples/templates/save_search_as_template/saveSearch.js: -------------------------------------------------------------------------------- 1 | const axlService = require("../../../index"); // Change this to require("cisco-axl") when using outside this package 2 | const path = require("path"); 3 | const fs = require("fs"); 4 | 5 | /* 6 | 7 | This script will use getPhone to retrieve an existing phone and save as a JSON file. This file can then be edited to use as a template for other operations. 8 | 9 | */ 10 | 11 | // Set up new AXL service (DevNet sandbox credentials: https://devnetsandbox.cisco.com/) 12 | let service = new axlService( 13 | "10.10.20.1", 14 | "administrator", 15 | "ciscopsdt", 16 | "14.0" 17 | ); 18 | 19 | (async () => { 20 | var operation = "getPhone"; 21 | var tags = await service.getOperationTags(operation); 22 | tags.name = "SEP112233445566"; 23 | 24 | // Options for executeOperation 25 | var opts = { 26 | clean: true, // Remove all null and empty tags 27 | removeAttributes: true // Remove all attributes and uuid's 28 | }; 29 | 30 | var returnPhoneTags = await service 31 | .executeOperation(operation, tags, opts) 32 | .catch((error) => { 33 | console.log(error); 34 | }); 35 | 36 | // Let's add some _data fields that we can use to add variables to our template. 37 | returnPhoneTags._data = { 38 | firstName: "Tom", 39 | lastName: "Smith", 40 | }; 41 | 42 | fs.writeFileSync( 43 | path.resolve(__dirname, "template.json"), 44 | JSON.stringify(returnPhoneTags, null, 2), 45 | (err) => { 46 | // throws an error, you could also catch it here 47 | if (err) throw err; 48 | // success case, the file was saved 49 | console.log("Template saved!"); 50 | } 51 | ); 52 | })(); 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cisco-axl", 3 | "version": "1.4.1", 4 | "description": "A library to make Cisco AXL a lot easier", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "scripts": { 8 | "build": "tsc", 9 | "prepare": "npm run build", 10 | "test": "NODE_OPTIONS=--experimental-vm-modules NODE_NO_WARNINGS=1 NODE_TLS_REJECT_UNAUTHORIZED=0 NODE_ENV=test node ./test/tests.js", 11 | "development": "NODE_OPTIONS=--experimental-vm-modules NODE_NO_WARNINGS=1 NODE_TLS_REJECT_UNAUTHORIZED=0 NODE_ENV=development node ./test/tests.js", 12 | "staging": "NODE_OPTIONS=--experimental-vm-modules NODE_NO_WARNINGS=1 NODE_TLS_REJECT_UNAUTHORIZED=0 NODE_ENV=staging node ./test/tests.js", 13 | "wsdl": "NODE_OPTIONS=--experimental-vm-modules NODE_NO_WARNINGS=1 NODE_TLS_REJECT_UNAUTHORIZED=0 NODE_ENV=staging node ./test/wsdl.js", 14 | "type-check": "tsc --noEmit", 15 | "ts-example": "ts-node examples/typescript/example.ts" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/sieteunoseis/cisco-axl.git" 20 | }, 21 | "keywords": [ 22 | "cisco", 23 | "node", 24 | "axl", 25 | "soap", 26 | "xml", 27 | "callmanager" 28 | ], 29 | "author": "Jeremy Worden", 30 | "license": "MIT", 31 | "bugs": { 32 | "url": "https://github.com/sieteunoseis/cisco-axl/issues" 33 | }, 34 | "homepage": "https://github.com/sieteunoseis/cisco-axl#readme", 35 | "dependencies": { 36 | "strong-soap": "^4.1.5" 37 | }, 38 | "devDependencies": { 39 | "@types/node": "^22.13.5", 40 | "dotenv": "*", 41 | "envalid": "*", 42 | "json-variables": "^10.1.0", 43 | "node-emoji": "*", 44 | "ts-node": "^10.9.2", 45 | "typescript": "^5.7.3" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /test/resetPhoneTest.js: -------------------------------------------------------------------------------- 1 | const axlService = require("../index"); 2 | const { cleanEnv, str, host, makeValidator } = require("envalid"); 3 | var path = require('path'); 4 | 5 | // If not production load the local env file 6 | if(process.env.NODE_ENV === "development"){ 7 | require('dotenv').config({ path: path.join(__dirname, '..', 'env', 'development.env') }) 8 | }else if(process.env.NODE_ENV === "test"){ 9 | require('dotenv').config({ path: path.join(__dirname, '..', 'env', 'test.env') }) 10 | }else if(process.env.NODE_ENV === "staging"){ 11 | require('dotenv').config({ path: path.join(__dirname, '..', 'env', 'staging.env') }) 12 | } 13 | 14 | const versionValid = makeValidator(x => { 15 | if (/.*\..*[^\\]/.test(x)) return x.toUpperCase() 16 | else throw new Error('CUCM_VERSION must be in the format of ##.#') 17 | }) 18 | 19 | const env = cleanEnv(process.env, { 20 | NODE_ENV: str({ 21 | choices: ["development", "test", "production", "staging"], 22 | desc: "Node environment", 23 | }), 24 | CUCM_HOSTNAME: host({ desc: "Cisco CUCM Hostname or IP Address." }), 25 | CUCM_USERNAME: str({ desc: "Cisco CUCM AXL Username." }), 26 | CUCM_PASSWORD: str({ desc: "Cisco CUCM AXL Password." }), 27 | CUCM_VERSION: versionValid({ desc: "Cisco CUCM Version." , example: "12.5" }) 28 | }); 29 | 30 | // Set up new AXL service 31 | let service = new axlService( 32 | env.CUCM_HOSTNAME, 33 | env.CUCM_USERNAME, 34 | env.CUCM_PASSWORD, 35 | env.CUCM_VERSION 36 | ); 37 | 38 | var operation = "resetPhone"; 39 | var tags = { 40 | "resetPhone": { 41 | "name": "SEP0038DFB50658" 42 | } 43 | }; 44 | 45 | service 46 | .executeOperation(operation, tags) 47 | .then((results) => { 48 | console.log(`${operation} UUID`, results); 49 | }) 50 | .catch((error) => { 51 | console.log(error); 52 | }); -------------------------------------------------------------------------------- /examples/templates/add_phone_update_line/addPhoneUpdateLine.js: -------------------------------------------------------------------------------- 1 | const axlService = require("../../../index"); // Change this to require("cisco-axl") when using outside this package 2 | const { jVar } = require("json-variables"); 3 | 4 | /* 5 | This script using json-variables (https://codsen.com/os/json-variables) to add a new phone from a template. 6 | The AXL "addPhone" operation will either add an existing line or add a new one. 7 | We will be using the "updateLine" to follow behind and update a few of the fields that "addPhone" does not include. 8 | 9 | Note: axlService is Promised based, so we using a nested promise. We wait for the "addPhone" promise to be fufilled before calling "updateLine". 10 | */ 11 | 12 | // Set up new AXL service (DevNet sandbox credentials: https://devnetsandbox.cisco.com/) 13 | let service = new axlService( 14 | "10.10.20.1", 15 | "administrator", 16 | "ciscopsdt", 17 | "14.0" 18 | ); 19 | 20 | // Read in the JSON templates 21 | var phoneTemplate = require("./phoneTemplate.json"); 22 | var lineTemplate = require("./lineTemplate.json"); 23 | 24 | (async () => { 25 | // Use json-variables to update our values from the template values 26 | const phoneArg = jVar(phoneTemplate); 27 | const lineArg = jVar(lineTemplate); 28 | 29 | // Call the first operation: "addPhone" with the jVar updated json 30 | service 31 | .executeOperation("addPhone", phoneArg) 32 | .then((results) => { 33 | // Print out the UUID for the successful "addPhone" call 34 | console.log("addPhone UUID", results); 35 | // Call the second operation: "update" with the jVar updated json 36 | service 37 | .executeOperation("updateLine", lineArg) 38 | .then((results) => { 39 | // Print out the UUID for the successful "updateLine" call 40 | console.log("updateLine UUID", results); 41 | }) 42 | .catch((error) => { 43 | console.log(error); 44 | }); 45 | }) 46 | .catch((error) => { 47 | console.log(error); 48 | }); 49 | })(); 50 | -------------------------------------------------------------------------------- /test/listCss.js: -------------------------------------------------------------------------------- 1 | const axlService = require("../index"); 2 | const { cleanEnv, str, host, makeValidator } = require("envalid"); 3 | var path = require('path'); 4 | const { jVar } = require("json-variables"); 5 | 6 | // If not production load the local env file 7 | if(process.env.NODE_ENV === "development"){ 8 | require('dotenv').config({ path: path.join(__dirname, '..', 'env', 'development.env') }) 9 | }else if(process.env.NODE_ENV === "test"){ 10 | require('dotenv').config({ path: path.join(__dirname, '..', 'env', 'test.env') }) 11 | }else if(process.env.NODE_ENV === "staging"){ 12 | require('dotenv').config({ path: path.join(__dirname, '..', 'env', 'staging.env') }) 13 | } 14 | 15 | const versionValid = makeValidator(x => { 16 | if (/.*\..*[^\\]/.test(x)) return x.toUpperCase() 17 | else throw new Error('CUCM_VERSION must be in the format of ##.#') 18 | }) 19 | 20 | const env = cleanEnv(process.env, { 21 | NODE_ENV: str({ 22 | choices: ["development", "test", "production", "staging"], 23 | desc: "Node environment", 24 | }), 25 | CUCM_HOSTNAME: host({ desc: "Cisco CUCM Hostname or IP Address." }), 26 | CUCM_USERNAME: str({ desc: "Cisco CUCM AXL Username." }), 27 | CUCM_PASSWORD: str({ desc: "Cisco CUCM AXL Password." }), 28 | CUCM_VERSION: versionValid({ desc: "Cisco CUCM Version." , example: "12.5" }) 29 | }); 30 | 31 | // Set up new AXL service 32 | let service = new axlService( 33 | env.CUCM_HOSTNAME, 34 | env.CUCM_USERNAME, 35 | env.CUCM_PASSWORD, 36 | env.CUCM_VERSION 37 | ); 38 | 39 | var operation = "listCss"; 40 | var cssTags = { 41 | "searchCriteria": { 42 | "description": "%", 43 | "partitionUsage": "%", 44 | "name": "%" 45 | }, 46 | "returnedTags": { 47 | "description": "", 48 | "clause": "", 49 | "dialPlanWizardGenId": "", 50 | "partitionUsage": "", 51 | "name": "" 52 | }, 53 | "skip": "", 54 | "first": "" 55 | } 56 | 57 | 58 | service 59 | .executeOperation(operation, cssTags) 60 | .then((results) => { 61 | console.log(results); 62 | }) 63 | .catch((error) => { 64 | console.log(error); 65 | }); -------------------------------------------------------------------------------- /test/updateLineTest.js: -------------------------------------------------------------------------------- 1 | const axlService = require("../index"); 2 | const { cleanEnv, str, host, makeValidator } = require("envalid"); 3 | var path = require('path'); 4 | const { jVar } = require("json-variables"); 5 | 6 | // If not production load the local env file 7 | if(process.env.NODE_ENV === "development"){ 8 | require('dotenv').config({ path: path.join(__dirname, '..', 'env', 'development.env') }) 9 | }else if(process.env.NODE_ENV === "test"){ 10 | require('dotenv').config({ path: path.join(__dirname, '..', 'env', 'test.env') }) 11 | }else if(process.env.NODE_ENV === "staging"){ 12 | require('dotenv').config({ path: path.join(__dirname, '..', 'env', 'staging.env') }) 13 | } 14 | 15 | const versionValid = makeValidator(x => { 16 | if (/.*\..*[^\\]/.test(x)) return x.toUpperCase() 17 | else throw new Error('CUCM_VERSION must be in the format of ##.#') 18 | }) 19 | 20 | const env = cleanEnv(process.env, { 21 | NODE_ENV: str({ 22 | choices: ["development", "test", "production", "staging"], 23 | desc: "Node environment", 24 | }), 25 | CUCM_HOSTNAME: host({ desc: "Cisco CUCM Hostname or IP Address." }), 26 | CUCM_USERNAME: str({ desc: "Cisco CUCM AXL Username." }), 27 | CUCM_PASSWORD: str({ desc: "Cisco CUCM AXL Password." }), 28 | CUCM_VERSION: versionValid({ desc: "Cisco CUCM Version." , example: "12.5" }) 29 | }); 30 | 31 | // Set up new AXL service 32 | let service = new axlService( 33 | env.CUCM_HOSTNAME, 34 | env.CUCM_USERNAME, 35 | env.CUCM_PASSWORD, 36 | env.CUCM_VERSION 37 | ); 38 | 39 | var operation = "updateLine"; 40 | 41 | var lineTemplate = { 42 | pattern: "%%_extension_%%", 43 | routePartitionName: "", 44 | alertingName: "%%_firstName_%% %%_lastName_%%", 45 | asciiAlertingName: "%%_firstName_%% %%_lastName_%%", 46 | description: "%%_firstName_%% %%_lastName_%%", 47 | _data: { 48 | extension: "\\+13758084010", 49 | firstName: "Jeremy", 50 | lastName: "Worden", 51 | }, 52 | }; 53 | 54 | const lineTags = jVar(lineTemplate); 55 | 56 | service 57 | .executeOperation(operation, lineTags) 58 | .then((results) => { 59 | console.log(results); 60 | }) 61 | .catch((error) => { 62 | console.log(error); 63 | }); -------------------------------------------------------------------------------- /examples/copy_sip_trunk/copySipTrunk.js: -------------------------------------------------------------------------------- 1 | const axlService = require("../../index"); // Change this to require("cisco-axl") when using outside this package 2 | 3 | /* 4 | Example of how to copy a SIP Trunk. Cisco doesn't have a Super Copy option for this, but we can do it via AXL. 5 | The new SIP trunk will need to have a new IP address for the destination address. 6 | 7 | Note: axlService is Promised based, so we using a nested promise. We wait for the first promise to be fufilled before calling the nested one. 8 | */ 9 | 10 | // Set up new AXL service 11 | let service = new axlService( 12 | "10.10.20.1", 13 | "administrator", 14 | "ciscopsdt", 15 | "14.0" 16 | ); 17 | 18 | (async () => { 19 | // First let's get the tags needed for finding a SIP Trunk 20 | var operation = "getSipTrunk"; 21 | var tags = await service.getOperationTags(operation); 22 | 23 | // Let's update the returned JSON with the name of the trunk we are trying to copy 24 | tags.name = "SIPTrunktoCUP"; 25 | 26 | // Make a call to AXL to get the information for the trunk we are copying. 27 | // Note: we will be sending the clean flag to executeOperation. This will remove any keys in our return json that is empty, undefined or null. 28 | var opts = { 29 | clean: true, 30 | removeAttributes: false, 31 | dataContainerIdentifierTails: "_data", 32 | }; 33 | 34 | var returnTrunk = await service.executeOperation(operation, tags, opts); 35 | 36 | // Update the JSON with our new values 37 | operation = "addSipTrunk"; 38 | returnTrunk.sipTrunk.name = "SIPTrunktoCUP2"; 39 | returnTrunk.sipTrunk.description = "NEW SIP Trunk"; 40 | returnTrunk.sipTrunk.destinations.destination[0].addressIpv4 = '10.10.20.18'; 41 | returnTrunk.sipTrunk.destinations.destination[0].port = '5060'; 42 | 43 | // Confidential Access Mode is returned from CUCM via AXL as undefined if not set on the phone we are copying. 44 | // We either need to set it to '' or delete it complete. Since we're not using this feature, let's delete it from our JSON. 45 | delete returnTrunk.sipTrunk.confidentialAccess; 46 | 47 | await service 48 | .executeOperation(operation, returnTrunk) 49 | .then((results) => { 50 | console.log("addSipTrunk UUID", results); 51 | }) 52 | .catch((error) => { 53 | console.log(error); 54 | }); 55 | })(); 56 | 57 | -------------------------------------------------------------------------------- /examples/typescript/example.ts: -------------------------------------------------------------------------------- 1 | import axlService from '../../src/index'; 2 | import { cleanEnv, str, host, makeValidator } from 'envalid'; 3 | import path from 'path'; 4 | import dotenv from 'dotenv'; 5 | 6 | // Load environment variables from the appropriate .env file 7 | if (process.env.NODE_ENV === "development") { 8 | dotenv.config({ path: path.join(__dirname, '..', '..', 'env', 'development.env') }); 9 | } else if (process.env.NODE_ENV === "test") { 10 | dotenv.config({ path: path.join(__dirname, '..', '..', 'env', 'test.env') }); 11 | } 12 | 13 | // Validator for CUCM version format 14 | const versionValid = makeValidator(x => { 15 | if (/.*\..*[^\\]/.test(x)) return x.toUpperCase(); 16 | else throw new Error('CUCM_VERSION must be in the format of ##.#'); 17 | }); 18 | 19 | // Clean and validate environment variables 20 | const env = cleanEnv(process.env, { 21 | NODE_ENV: str({ 22 | choices: ["development", "test", "production", "staging"], 23 | desc: "Node environment", 24 | default: "development" 25 | }), 26 | CUCM_HOSTNAME: host({ 27 | desc: "Cisco CUCM Hostname or IP Address.", 28 | default: "cucm01-pub.automate.builder" 29 | }), 30 | CUCM_USERNAME: str({ 31 | desc: "Cisco CUCM AXL Username.", 32 | default: "perfmon" 33 | }), 34 | CUCM_PASSWORD: str({ 35 | desc: "Cisco CUCM AXL Password.", 36 | default: "perfmon" 37 | }), 38 | CUCM_VERSION: versionValid({ 39 | desc: "Cisco CUCM Version.", 40 | example: "12.5", 41 | default: "15.0" 42 | }) 43 | }); 44 | 45 | async function main() { 46 | try { 47 | // Initialize AXL service using environment variables 48 | const service = new axlService( 49 | env.CUCM_HOSTNAME, 50 | env.CUCM_USERNAME, 51 | env.CUCM_PASSWORD, 52 | env.CUCM_VERSION 53 | ); 54 | 55 | // Example of adding a route partition 56 | const operation = "addRoutePartition"; 57 | const tags = { 58 | routePartition: { 59 | name: "TYPESCRIPT-PT", 60 | description: "Created with TypeScript", 61 | timeScheduleIdName: "", 62 | useOriginatingDeviceTimeZone: "", 63 | timeZone: "", 64 | partitionUsage: "", 65 | }, 66 | }; 67 | 68 | console.log("Adding route partition..."); 69 | const result = await service.executeOperation(operation, tags); 70 | console.log("Route partition added with UUID:", result); 71 | 72 | // Example of listing route partitions 73 | const listOperation = "listRoutePartition"; 74 | const listTags = await service.getOperationTags(listOperation); 75 | listTags.searchCriteria.name = "%%"; 76 | 77 | console.log("Listing route partitions..."); 78 | const partitions = await service.executeOperation(listOperation, listTags); 79 | 80 | // Display the partitions 81 | console.log("Route partitions:"); 82 | if (Array.isArray(partitions.routePartition)) { 83 | partitions.routePartition.forEach((partition: any) => { 84 | console.log(`- ${partition.name}`); 85 | }); 86 | } else if (partitions.routePartition) { 87 | console.log(`- ${partitions.routePartition.name}`); 88 | } 89 | } catch (error) { 90 | console.error("Error:", error); 91 | } 92 | } 93 | 94 | main(); -------------------------------------------------------------------------------- /examples/templates/update_phone_update_line/updatePhoneUpdateLine.js: -------------------------------------------------------------------------------- 1 | const axlService = require("../../../index"); // Change this to require("cisco-axl") when using outside this package 2 | const { jVar } = require("json-variables"); 3 | 4 | /* 5 | Every wanted to change an exsiting phone from one user to another user? This script will help you do that as well as updating all the display, 6 | line text labels, etc for the new user. 7 | 8 | This script using json-variables (https://codsen.com/os/json-variables) to update a phone from a template. 9 | 10 | We will be updating a phone from an existing user to a new user. This is a common MACD change for help desks. 11 | - First we will use the "getUser" operation to return some values for the user we will be using to replace the existing user. 12 | - Next we will take those values and update some of templates values. 13 | - Next we will use jVar to merge those values for us so we can send them via AXL. 14 | 15 | Note: axlService is Promised based, so we using a nested promise. We wait for the first promise to be fufilled before calling the nested one. 16 | */ 17 | 18 | // Set up new AXL service (DevNet sandbox credentials: https://devnetsandbox.cisco.com/) 19 | let service = new axlService( 20 | "10.10.20.1", 21 | "administrator", 22 | "ciscopsdt", 23 | "14.0" 24 | ); 25 | 26 | // Read in the JSON templates 27 | var phoneTemplate = require("./phoneTemplate.json"); 28 | var lineTemplate = require("./lineTemplate.json"); 29 | 30 | // Set up the tags for our "getUser" call. We are interested in getting the first and last name to use in our template later. 31 | 32 | var deviceToUpdate = { 33 | deviceName: "CSFUSER005", 34 | extension: "1005" 35 | }; 36 | 37 | // User to update phone value to 38 | 39 | var getUserJSON = { 40 | userid: "user03", 41 | returnedTags: { 42 | firstName: "", 43 | lastName: "", 44 | }, 45 | }; 46 | 47 | (async () => { 48 | // Call getUser operation and store in the userInfo variable 49 | var userInfo = await service 50 | .executeOperation("getUser", getUserJSON) 51 | .catch((error) => { 52 | console.log(error); 53 | }); 54 | 55 | // Let's update our variable data with the information that we got back from our AXL call 56 | phoneTemplate._data.firstName = userInfo.user.firstName; 57 | phoneTemplate._data.lastName = userInfo.user.lastName; 58 | phoneTemplate._data.userid = getUserJSON.userid; 59 | phoneTemplate._data.deviceName = deviceToUpdate.deviceName; 60 | phoneTemplate._data.extension = deviceToUpdate.extension; 61 | lineTemplate._data.firstName = userInfo.user.firstName; 62 | lineTemplate._data.lastName = userInfo.user.lastName; 63 | lineTemplate._data.extension = deviceToUpdate.extension; 64 | 65 | // Use json-variables to update our values from the template values 66 | const phoneTags = jVar(phoneTemplate); 67 | const lineTags = jVar(lineTemplate); 68 | 69 | // Call the first operation: "updatePhone" with the jVar updated json 70 | service 71 | .executeOperation("updatePhone", phoneTags) 72 | .then((results) => { 73 | // Print out the UUID for the successful "updatePhone" call 74 | console.log("updatePhone UUID", results); 75 | // Call the second operation: "updateLine" with the jVar updated json 76 | service 77 | .executeOperation("updateLine", lineTags) 78 | .then((results) => { 79 | // Print out the UUID for the successful "updateLine" call 80 | console.log("updateLine UUID", results); 81 | // Lastly let's update our user with the controlled device and set the primary ex 82 | var updateUserJSON = { 83 | userid: getUserJSON.userid, 84 | associatedDevices: { 85 | device: [deviceToUpdate.deviceName], 86 | }, 87 | primaryExtension: { pattern: deviceToUpdate.extension, routePartitionName: '' } 88 | }; 89 | service 90 | .executeOperation("updateUser", updateUserJSON) 91 | .then((results) => { 92 | // Print out the UUID for the successful "updateUser" call 93 | console.log("updateUser UUID", results); 94 | }) 95 | .catch((error) => { 96 | console.log(error); 97 | }); 98 | }) 99 | .catch((error) => { 100 | console.log(error); 101 | }); 102 | }) 103 | .catch((error) => { 104 | console.log(error); 105 | }); 106 | })(); 107 | -------------------------------------------------------------------------------- /examples/templates/add_phone_update_line/phoneTemplate.json: -------------------------------------------------------------------------------- 1 | { 2 | "phone":{ 3 | "name":"%%_deviceName_%%", 4 | "description":"%%_lastName_%%, %%_firstName_%% Extension %%_extension_%%", 5 | "product":"Cisco Unified Client Services Framework", 6 | "model":"Cisco Unified Client Services Framework", 7 | "class":"Phone", 8 | "protocol":"SIP", 9 | "protocolSide":"User", 10 | "devicePoolName":{ 11 | "value":"Default" 12 | }, 13 | "commonPhoneConfigName":{ 14 | "value":"Standard Common Phone Profile" 15 | }, 16 | "networkLocation":"Use System Default", 17 | "locationName":{ 18 | "value":"Hub_None" 19 | }, 20 | "loadInformation":{ 21 | "attributes":{ 22 | "special":"false" 23 | } 24 | }, 25 | "versionStamp":"", 26 | "traceFlag":"false", 27 | "mlppIndicationStatus":"Off", 28 | "preemption":"Disabled", 29 | "useTrustedRelayPoint":"Default", 30 | "retryVideoCallAsAudio":"true", 31 | "securityProfileName":{ 32 | "value":"Cisco Unified Client Services Framework - UDP" 33 | }, 34 | "sipProfileName":{ 35 | "value":"Standard SIP Profile" 36 | }, 37 | "useDevicePoolCgpnTransformCss":"true", 38 | "sendGeoLocation":"false", 39 | "lines":{ 40 | "line":[ 41 | { 42 | "index":"1", 43 | "label":"%%_lastName_%% - %%_extension_%%", 44 | "display":"%%_lastName_%%, %%_firstName_%%", 45 | "dirn":{ 46 | "pattern":"%%_extension_%%" 47 | }, 48 | "ringSetting":"Ring", 49 | "consecutiveRingSetting":"Use System Default", 50 | "mwlPolicy":"Use System Policy", 51 | "displayAscii":"%%_lastName_%%, %%_firstName_%%", 52 | "maxNumCalls":"3", 53 | "busyTrigger":"2", 54 | "callInfoDisplay":{ 55 | "callerName":"true", 56 | "callerNumber":"false", 57 | "redirectedNumber":"false", 58 | "dialedNumber":"true" 59 | }, 60 | "recordingFlag":"Call Recording Disabled", 61 | "audibleMwi":"Default", 62 | "partitionUsage":"General", 63 | "associatedEndusers":{ 64 | "enduser":[ 65 | { 66 | "userId":"%%_userid_%%" 67 | } 68 | ] 69 | }, 70 | "missedCallLogging":"true", 71 | "recordingMediaSource":"Gateway Preferred" 72 | } 73 | ] 74 | }, 75 | "numberOfButtons":"1", 76 | "phoneTemplateName":{ 77 | "value":"Standard Client Services Framework" 78 | }, 79 | "ringSettingIdleBlfAudibleAlert":"Default", 80 | "ringSettingBusyBlfAudibleAlert":"Default", 81 | "enableExtensionMobility":"false", 82 | "currentProfileName":{ 83 | 84 | }, 85 | "currentConfig":{ 86 | "phoneTemplateName":{ 87 | "value":"Standard Client Services Framework" 88 | }, 89 | "mlppIndicationStatus":"Off", 90 | "preemption":"Disabled", 91 | "ignorePresentationIndicators":"false", 92 | "singleButtonBarge":"Default", 93 | "joinAcrossLines":"Off", 94 | "callInfoPrivacyStatus":"Default", 95 | "dndRingSetting":"Disable", 96 | "dndOption":"Ringer Off", 97 | "alwaysUsePrimeLine":"Default", 98 | "alwaysUsePrimeLineForVoiceMessage":"Default", 99 | "emccCallingSearchSpaceName":{ 100 | 101 | } 102 | }, 103 | "singleButtonBarge":"Default", 104 | "joinAcrossLines":"Off", 105 | "builtInBridgeStatus":"Default", 106 | "callInfoPrivacyStatus":"Default", 107 | "hlogStatus":"On", 108 | "ignorePresentationIndicators":"false", 109 | "packetCaptureMode":"None", 110 | "packetCaptureDuration":"0", 111 | "allowCtiControlFlag":"true", 112 | "presenceGroupName":{ 113 | "value":"Standard Presence group" 114 | }, 115 | "unattendedPort":"false", 116 | "requireDtmfReception":"false", 117 | "rfc2833Disabled":"false", 118 | "certificateOperation":"No Pending Operation", 119 | "certificateStatus":"None", 120 | "deviceMobilityMode":"Default", 121 | "remoteDevice":"false", 122 | "dndOption":"Ringer Off", 123 | "dndRingSetting":"Disable", 124 | "dndStatus":"false", 125 | "isActive":"true", 126 | "isDualMode":"true", 127 | "phoneSuite":"Default", 128 | "phoneServiceDisplay":"Default", 129 | "isProtected":"false", 130 | "mtpRequired":"false", 131 | "mtpPreferedCodec":"711ulaw", 132 | "outboundCallRollover":"No Rollover", 133 | "hotlineDevice":"false", 134 | "alwaysUsePrimeLine":"Default", 135 | "alwaysUsePrimeLineForVoiceMessage":"Default", 136 | "deviceTrustMode":"Not Trusted", 137 | "AllowPresentationSharingUsingBfcp":"false", 138 | "allowiXApplicableMedia":"false", 139 | "useDevicePoolCgpnIngressDN":"true", 140 | "enableCallRoutingToRdWhenNoneIsActive":"f", 141 | "enableActivationID":"false", 142 | "allowMraMode":"false" 143 | }, 144 | "phone_data":{ 145 | "firstName":"Tom", 146 | "lastName":"Smith", 147 | "extension":"2001", 148 | "userid":"user01", 149 | "deviceName": "CSFSMITHT" 150 | } 151 | } -------------------------------------------------------------------------------- /test/tests.js: -------------------------------------------------------------------------------- 1 | const axlService = require("../index"); 2 | const emoji = require("node-emoji"); 3 | const { cleanEnv, str, host, makeValidator } = require("envalid"); 4 | var path = require('path'); 5 | 6 | // If not production load the local env file 7 | if(process.env.NODE_ENV === "development"){ 8 | require('dotenv').config({ path: path.join(__dirname, '..', 'env', 'development.env') }) 9 | }else if(process.env.NODE_ENV === "test"){ 10 | require('dotenv').config({ path: path.join(__dirname, '..', 'env', 'test.env') }) 11 | }else if(process.env.NODE_ENV === "staging"){ 12 | require('dotenv').config({ path: path.join(__dirname, '..', 'env', 'staging.env') }) 13 | } 14 | 15 | const versionValid = makeValidator(x => { 16 | if (/.*\..*[^\\]/.test(x)) return x.toUpperCase() 17 | else throw new Error('CUCM_VERSION must be in the format of ##.#') 18 | }) 19 | 20 | const env = cleanEnv(process.env, { 21 | NODE_ENV: str({ 22 | choices: ["development", "test", "production", "staging"], 23 | desc: "Node environment", 24 | }), 25 | CUCM_HOSTNAME: host({ desc: "Cisco CUCM Hostname or IP Address." }), 26 | CUCM_USERNAME: str({ desc: "Cisco CUCM AXL Username." }), 27 | CUCM_PASSWORD: str({ desc: "Cisco CUCM AXL Password." }), 28 | CUCM_VERSION: versionValid({ desc: "Cisco CUCM Version." , example: "12.5" }) 29 | }); 30 | 31 | // Set up new AXL service 32 | let service = new axlService( 33 | env.CUCM_HOSTNAME, 34 | env.CUCM_USERNAME, 35 | env.CUCM_PASSWORD, 36 | env.CUCM_VERSION 37 | ); 38 | 39 | var check = emoji.get("heavy_check_mark"); 40 | var cat = emoji.get("smiley_cat"); 41 | var skull = emoji.get("skull"); 42 | var sparkles = emoji.get("sparkles"); 43 | var spy = emoji.get("female_detective"); 44 | var next = emoji.get("next_track_button"); 45 | var list = emoji.get("spiral_notepad"); 46 | var computer = emoji.get("computer"); 47 | var finished = emoji.get("raised_hand"); 48 | 49 | (async () => { 50 | console.log(`${spy} Let's first get a list of all the operations.`); 51 | var operationArr = await service.returnOperations(); 52 | console.log(computer, "Found", operationArr.length,"operations."); 53 | const random = Math.floor(Math.random() * operationArr.length); 54 | console.log(`${spy} Let's pick out out at random operation: '`,operationArr[random],"'"); 55 | console.log( 56 | `${next} Now let's get a list of all the operations that include the word 'partition'.` 57 | ); 58 | var operationFilteredArr = await service.returnOperations("partition"); 59 | console.log(computer, operationFilteredArr); 60 | 61 | console.log( 62 | `${cat} Great. Let's add a new route partition via 'addRoutePartition' operation.` 63 | ); 64 | 65 | var operation = "addRoutePartition"; 66 | console.log( 67 | `${next} We'll need to get what tags to pass to the SOAP client first.` 68 | ); 69 | var tags = await service.getOperationTags(operation); 70 | console.log(computer, tags); 71 | console.log( 72 | `${sparkles} Magnificent, let's update the name and description fields.` 73 | ); 74 | tags.routePartition.name = "TEST-PARTITION-PT"; 75 | tags.routePartition.description = "Partition for testing purposes. Created by AXL."; 76 | console.log(computer, tags); 77 | console.log( 78 | `${next} Now we will attempt to add the new partition. Function should return an UUID to represent the new partition.` 79 | ); 80 | await service 81 | .executeOperation(operation, tags) 82 | .then(async (results) => { 83 | console.log(computer, results); 84 | console.log( 85 | `${cat} Awesome, let's get a list of all the partitions on our cluster now. First we'll get the tags needed for this call.` 86 | ); 87 | 88 | operation = "listRoutePartition"; 89 | tags = await service.getOperationTags(operation); 90 | console.log(computer, tags); 91 | 92 | console.log( 93 | `${spy} We want to list all partitions, so we'll be searching for a wildcard (%%) in the name and description fields.` 94 | ); 95 | tags.searchCriteria.name = "%%"; 96 | tags.searchCriteria.description = "%%"; 97 | console.log(computer, tags); 98 | console.log( 99 | `${sparkles} Astounding, now that we've updated our tags, we'll send the AXL request via SOAP.` 100 | ); 101 | 102 | await service 103 | .executeOperation(operation, tags) 104 | .then((results) => { 105 | console.log( 106 | `${list} Here are a list of all the partitions on our cluster:` 107 | ); 108 | results.routePartition.map((str) => { 109 | var outString = `${check} ${str.name}`; 110 | console.log(outString); 111 | }); 112 | }) 113 | .catch((error) => { 114 | console.log(skull, error); 115 | }); 116 | }) 117 | .catch((error) => { 118 | console.log(skull, "Adding a new partition failed", error); 119 | }); 120 | 121 | 122 | var operation = "removeRoutePartition"; 123 | console.log( 124 | `${skull} Now let's remove the partition we just created. We'll need to get what tags to pass to the SOAP client first.` 125 | ); 126 | var tags = await service.getOperationTags(operation); 127 | console.log(computer, tags); 128 | console.log( 129 | `${sparkles} Magnificent, let's update the name of the partition we wanted to remove.` 130 | ); 131 | tags.name = "TEST-PARTITION-PT"; 132 | console.log(computer, tags); 133 | console.log( 134 | `${next} Now we will attempt to delete the partition. Function should return an UUID.` 135 | ); 136 | await service 137 | .executeOperation(operation, tags) 138 | .then(async (results) => { 139 | console.log(computer, results); 140 | console.log( 141 | `${cat} Awesome, let's get a list of all the partitions on our cluster now. First we'll get the tags needed for this call.` 142 | ); 143 | 144 | operation = "listRoutePartition"; 145 | tags = await service.getOperationTags(operation); 146 | console.log(computer, tags); 147 | 148 | console.log( 149 | `${spy} We want to list all partitions, so we'll be searching for a wildcard (%%) in the name and description fields.` 150 | ); 151 | tags.searchCriteria.name = "%%"; 152 | tags.searchCriteria.description = "%%"; 153 | console.log(computer, tags); 154 | console.log( 155 | `${sparkles} Astounding, now that we've updated our tags, we'll send the AXL request via SOAP.` 156 | ); 157 | 158 | await service 159 | .executeOperation(operation, tags) 160 | .then((results) => { 161 | console.log( 162 | `${list} Here are an updated list of all the partitions on our cluster (Notice our partition we created in an earlier step is missing):` 163 | ); 164 | results.routePartition.map((str) => { 165 | var outString = `${check} ${str.name}`; 166 | console.log(outString); 167 | }); 168 | }) 169 | .catch((error) => { 170 | console.log(skull, error); 171 | }); 172 | }) 173 | .catch((error) => { 174 | console.log(skull, "Deleteing partition failed", error); 175 | }); 176 | 177 | console.log(finished, "Test all finished. Thanks!"); 178 | })(); 179 | -------------------------------------------------------------------------------- /examples/change_phone_model/changePhoneModel.js: -------------------------------------------------------------------------------- 1 | const axlService = require("../../index"); // Change this to require("cisco-axl") when using outside this package 2 | 3 | /* 4 | Ever wanted to change a phone model, but keep the existing one in CUCM? This script will attempt to migrate a phone to the new model. 5 | It uses SQL queries to grab some of the product specific information for the new model. 6 | 7 | We will be using axl to "getPhone" information and then make changes before calling the "addPhone" operation. 8 | - First we will use the "getPhone" operation to return some values for the phone we will be using to copy. 9 | - Next we will be using a number of SQL query's to validate settings needed for the new phone model 10 | - Lastly we will be combining all this information so we can send them via AXL. 11 | 12 | Note: axlService is Promised based, so we using a nested promise. We wait for the first promise to be fufilled before calling the nested one. 13 | */ 14 | 15 | // Set up new AXL service 16 | let service = new axlService( 17 | "10.10.20.1", 18 | "administrator", 19 | "ciscopsdt", 20 | "14.0" 21 | ); 22 | 23 | (async () => { 24 | 25 | // Variable needed for script 26 | var opts = { 27 | clean: true, 28 | removeAttributes: false, 29 | dataContainerIdentifierTails: "_data", 30 | }; 31 | 32 | // What model are we going to convert our phone into? 33 | // You can get a list of models in CUCM with the SQL query: 'SELECT name from typemodel' 34 | var convertPhoneType = "Cisco 8865"; // Another example: Cisco Dual Mode for Android 35 | var phoneNameCopyingFrom = "CSFUSER001"; 36 | var phoneNameCopyingTo = "SEP112233445566"; // Another example: BOTWORDENJ 37 | var newPhoneDescription = "Test phone added via AXL"; 38 | 39 | // We're going to use the getPhone operation to retrieve the settings from the phone we want to copy. First we'll get the JSON arguments needed for our AXL call. 40 | var operation = "getPhone"; 41 | var tags = await service.getOperationTags(operation); 42 | // Print out our operation tags 43 | console.log(tags); 44 | // Update the name of phone that we want to copy 45 | tags.name = phoneNameCopyingFrom; 46 | 47 | // Execute the AXL call. We set the true flag to clean the output (removes any blank or undefined settings) 48 | var returnPhoneTags = await service.executeOperation(operation, tags, opts); 49 | 50 | // Now we're going to add a new phone based on the settings. We will updating a few new settings based on the model we will be converting to. 51 | operation = "addPhone"; 52 | returnPhoneTags.phone.name = phoneNameCopyingTo; // New phone name that we will be using 53 | returnPhoneTags.phone.description = newPhoneDescription; // Add a description 54 | returnPhoneTags.phone.product = convertPhoneType; // Model product and type 55 | returnPhoneTags.phone.model = convertPhoneType; 56 | 57 | // Next we'll be pulling some of the supported tags for our new phone type. 58 | // This may not be all the settings you have to change when converting to a new model, but are the most common that I've run into. 59 | 60 | // Phone button templates are device and protocol specific. 61 | // Let's get a list of the templates configured on our cluster that we can use. 62 | 63 | var phoneTemplateSQL = `SELECT model.name AS device, pt.name AS template, p.buttonnum, tf.name AS feature, dp.name AS protocol 64 | FROM phonetemplate AS pt, phonebutton AS p, typemodel AS model, typefeature AS tf, typedeviceprotocol AS dp 65 | WHERE pt.pkid = p.fkphonetemplate AND pt.tkmodel = model.enum AND pt.tkdeviceprotocol = dp.enum AND p.tkfeature = tf.enum 66 | AND model.name='${convertPhoneType}'`; 67 | 68 | operation = "executeSQLQuery"; 69 | tags = await service.getOperationTags(operation); 70 | tags.sql = phoneTemplateSQL; 71 | var phoneTemplate = await service.executeOperation(operation, tags); 72 | 73 | // This is for SIP phones. 74 | // Let's get a list of Security Profiles configured on our cluster that we can use. 75 | 76 | var securityProfileNameSQL = `SELECT model.name AS device, sp.name AS PROFILE 77 | FROM securityprofile sp 78 | LEFT OUTER JOIN typemodel AS model ON sp.tkmodel = model.enum 79 | WHERE (sp.tksecuritypolicy = 4 OR sp.tksecuritypolicy = 99) AND model.name = '${convertPhoneType}' 80 | ORDER BY model.name`; 81 | 82 | operation = "executeSQLQuery"; 83 | tags = await service.getOperationTags(operation); 84 | tags.sql = securityProfileNameSQL; 85 | var securityProfileName = await service.executeOperation(operation, tags); 86 | 87 | // DND Option can only be set to non-Zero on devices that support the DND feature (in 88 | // ProductSupportsFeature table). For those devices that support the feature, only the Ringer Off (0) is 89 | // valid, unless a parameter is present in the PSF record. If a parameter value of 1 exists in PSF table, only 90 | // Call Reject is valid. If the param value is (2), all options including Use Common Profile (2) are valid. 91 | // Dual mode and remote destination profile only support the Call Reject option. 92 | 93 | var dndOptionSQL = `select model.name,dp.name as protocol,p.param from typemodel as model, 94 | typedeviceprotocol as dp,ProductSupportsFeature as p where p.tkmodel=model.enum and p. 95 | tkSupportsFeature=(select enum from typesupportsfeature where name='Do Not Disturb') 96 | and p.tkdeviceprotocol=dp.enum and model.tkclass=(select enum from typeclass where name= 97 | 'Phone')and model.name='${convertPhoneType}' order by model.name`; 98 | 99 | operation = "executeSQLQuery"; 100 | tags = await service.getOperationTags(operation); 101 | tags.sql = dndOptionSQL; 102 | var dndOption = await service.executeOperation(operation, tags); 103 | 104 | // Maximum Number of Calls and Busy Trigger (Less than or equal to Max. Calls) values depend on model type. 105 | // We'll use an SQL query to figure out what values we need for our model type. 106 | // 200:4:2 (Max calls per device:default max calls per line:default busy trigger). 107 | 108 | var maxBusySQL = `SELECT NAME,param 109 | FROM typemodel AS model, productsupportsfeature AS p 110 | WHERE p.tkmodel = model.enum 111 | AND p.tksupportsfeature = (SELECT enum FROM typesupportsfeature WHERE NAME = 'Multiple Call Display') 112 | AND model.tkclass = (SELECT enum FROM typeclass WHERE NAME = 'Phone') AND name='${convertPhoneType}'`; 113 | 114 | operation = "executeSQLQuery"; 115 | tags = await service.getOperationTags(operation); 116 | tags.sql = maxBusySQL; 117 | var maxBusy = await service.executeOperation(operation, tags); 118 | 119 | // Let's update our JSON with the necessary values from above. Note some will return multiple values. You may need to loop thru and find the one you want. 120 | 121 | returnPhoneTags.phone.phoneTemplateName.value = phoneTemplate.row[0].template; 122 | returnPhoneTags.phone.securityProfileName.value = securityProfileName.row[0].profile; 123 | 124 | // Does this phone support DND? If so does it have a param value of 1 125 | if(dndOption){ 126 | if(dndOption.row[0].param === '1'){ 127 | returnPhoneTags.phone.dndOption = "Call Reject"; 128 | }else{ 129 | returnPhoneTags.phone.dndOption = "Ringer Off"; 130 | } 131 | }else{ 132 | delete returnPhoneTags.phone.dndOption; 133 | } 134 | 135 | // Let's split up our values we got back from CUCM. We'll be setting each line to the default for Max Number of Calls and minus one for our Busy Trigger. 136 | var maxBusyArr = maxBusy.row[0].param.split(':'); 137 | 138 | // Loop thru all lines configured and update as needed. 139 | 140 | returnPhoneTags.phone.lines.line.map((object) => { 141 | object.maxNumCalls = maxBusyArr[2]; 142 | object.busyTrigger = maxBusyArr[2] - 1; 143 | }); 144 | 145 | // Confidential Access Mode is returned from CUCM via AXL as undefined if not set on the phone we are copying. 146 | // We either need to set it to '' or delete it complete. Since we're not using this feature, let's delete it from our JSON. 147 | 148 | delete returnPhoneTags.phone.confidentialAccess; 149 | 150 | // Print out updated JSON before we execute addPhone on CUCM via AXL 151 | 152 | console.log(returnPhoneTags); 153 | 154 | // Let's add our new phone. If successful we should get back a UUID of our new phone. 155 | operation = "addPhone"; 156 | await service 157 | .executeOperation(operation, returnPhoneTags) 158 | .then((results) => { 159 | console.log("New UUID",results); 160 | }) 161 | .catch((error) => { 162 | console.log(error); 163 | }); 164 | })(); 165 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cisco AXL SOAP Library 2 | 3 | A Javascript library to pull AXL data Cisco CUCM via SOAP. The goal of this project is to make it easier for people to use AXL and to include all functionality of AXL. This library utilizes [strong-soap](https://www.npmjs.com/package/strong-soap) to read Cisco's WSDL file. As a result this library can use any function in the schema for the version that you specify. 4 | 5 | Administrative XML (AXL) information can be found at: 6 | [Administrative XML (AXL) Reference](https://developer.cisco.com/docs/axl/#!axl-developer-guide). 7 | 8 | ## Installation 9 | 10 | Using npm: 11 | 12 | ```javascript 13 | npm i -g npm 14 | npm i --save cisco-axl 15 | ``` 16 | 17 | ## Requirements 18 | 19 | This package uses the built in Fetch API of Node. This feature was first introduced in Node v16.15.0. You may need to enable expermential vm module. Also you can disable warnings with an optional enviromental variable. 20 | 21 | Also if you are using self signed certificates on Cisco VOS products you may need to disable TLS verification. This makes TLS, and HTTPS by extension, insecure. The use of this environment variable is strongly discouraged. Please only do this in a lab enviroment. 22 | 23 | Suggested enviromental variables: 24 | 25 | ```env 26 | NODE_OPTIONS=--experimental-vm-modules 27 | NODE_NO_WARNINGS=1 28 | NODE_TLS_REJECT_UNAUTHORIZED=0 29 | ``` 30 | 31 | ### Debugging 32 | 33 | You can enable debug logging by setting the `DEBUG` environment variable to any truthy value (except 'false', 'no', '0', 'off', or 'n'). When enabled, debug logs will show detailed information about authentication tests and operations being executed. 34 | 35 | ```env 36 | DEBUG=true 37 | ``` 38 | 39 | Debug logs will be prefixed with `[AXL DEBUG]` and include information such as: 40 | - Authentication test attempts and responses 41 | - Operations being executed 42 | - API responses 43 | 44 | This is especially helpful when troubleshooting authentication issues or unexpected API behavior. 45 | 46 | ## Features 47 | 48 | - This library uses strong-soap to parse the AXL WSDL file. As a result any AXL function for your specified version is avaliable to use! 49 | - Supports the Promise API. Can chain procedures together or you could use Promise.all() to run multiple "get" operations at the same time. 50 | - Returns all results in JSON rather than XML. Function has options to remove all blank or empty fields from JSON results via optional clean parameter. 51 | - Support for [json-variables](https://codsen.com/os/json-variables). The executeOperation function will recognize the dataContainerIdentifierTails from json-variables and remove them from your call. This avoids any SOAP fault issues from having extra information in call. See examples folder for use case. 52 | - TypeScript support with type definitions for better developer experience and code reliability 53 | - Authentication testing with the testAuthentication method to verify credentials before executing operations 54 | - Debug logging capabilities to help troubleshoot API interactions by setting the DEBUG environment variable 55 | 56 | ## Usage 57 | 58 | ```javascript 59 | const axlService = require("cisco-axl"); 60 | 61 | let service = new axlService("10.10.20.1", "administrator", "ciscopsdt","14.0"); 62 | 63 | var operation = "addRoutePartition"; 64 | var tags = { 65 | routePartition: { 66 | name: "INTERNAL-PT", 67 | description: "Internal directory numbers", 68 | timeScheduleIdName: "", 69 | useOriginatingDeviceTimeZone: "", 70 | timeZone: "", 71 | partitionUsage: "", 72 | }, 73 | }; 74 | 75 | service 76 | .executeOperation(operation, tags) 77 | .then((results) => { 78 | console.log("addRoutePartition UUID", results); 79 | }) 80 | .catch((error) => { 81 | console.log(error); 82 | }); 83 | ``` 84 | 85 | ## Methods 86 | 87 | - new axlService(options: obj) 88 | - axlService.testAuthentication() 89 | - axlService.returnOperations(filter?: string) 90 | - axlService.getOperationTags(operation: string) 91 | - axlService.executeOperation(operation: string,tags: obj, opts?: obj) 92 | 93 | ### new axlService(options) 94 | 95 | Service constructor for methods. Requires a JSON object consisting of hostname, username, password and version. 96 | 97 | ```node 98 | let service = new axlService("10.10.20.1", "administrator", "ciscopsdt", "14.0"); 99 | ``` 100 | 101 | ### service.testAuthentication() ⇒ Returns promise 102 | 103 | Tests the authentication credentials against the AXL endpoint. Returns a promise that resolves to `true` if authentication is successful, or rejects with an error if authentication fails. 104 | 105 | ```node 106 | service.testAuthentication() 107 | .then((success) => { 108 | console.log('Authentication successful'); 109 | }) 110 | .catch((error) => { 111 | console.error('Authentication failed:', error.message); 112 | }); 113 | ``` 114 | 115 | ### service.returnOperations(filter?) ⇒ Returns promise 116 | 117 | Method takes optional argument to filter results. No argument returns all operations. Returns results via Promise. 118 | 119 | | Method | Argument | Type | Obligatory | Description | 120 | | :--------------- | :------- | :----- | :--------- | :---------------------------------- | 121 | | returnOperations | filter | string | No | Provide a string to filter results. | 122 | 123 | ### service.getOperationTags(operation) ⇒ Returns promise 124 | 125 | Method requires passing an AXL operation. Returns results via Promise. 126 | 127 | | Method | Argument | Type | Obligatory | Description | 128 | | :--------------- | :-------- | :----- | :--------- | :----------------------------------------------------------------------- | 129 | | getOperationTags | operation | string | Yes | Provide the name of the AXL operation you wish to retrieve the tags for. | 130 | 131 | ### service.executeOperation(operation,tags,opts?) ⇒ Returns promise 132 | 133 | Method requires passing an AXL operation and JSON object of tags. Returns results via Promise. 134 | 135 | Current options include: 136 | | option | type | description | 137 | | :--------------------------- | :------ | :---------------------------------------------------------------------------------- | 138 | | clean | boolean | Default: **false**. Allows method to remove all tags that have no values from return data. | 139 | | removeAttributes | boolean | Default: **false**. Allows method to remove all attributes tags return data. | 140 | | dataContainerIdentifierTails | string | Default: **'\_data'**. executeOperation will automatically remove any tag with the defined string. This is used with json-variables library. | 141 | 142 | Example: 143 | 144 | ```node 145 | var opts = { 146 | clean: true, 147 | removeAttributes: false, 148 | dataContainerIdentifierTails: "_data", 149 | }; 150 | ``` 151 | 152 | | Method | Argument | Type | Obligatory | Description | 153 | | :--------------- | :-------- | :----- | :--------- | :--------------------------------------------------------- | 154 | | executeOperation | operation | string | Yes | Provide the name of the AXL operation you wish to execute. | 155 | | executeOperation | tags | object | Yes | Provide a JSON object of the tags for your operation. | 156 | | executeOperation | opts | object | No | Provide a JSON object of options for your operation. | 157 | 158 | ## Examples 159 | 160 | Check **examples** folder for different ways to use this library. Each folder should have a **README** to explain about each example. 161 | 162 | You can also run the **tests.js** against Cisco's DevNet sandbox so see how each various method works. 163 | 164 | ```javascript 165 | npm run test 166 | ``` 167 | 168 | Note: Test are using Cisco's DevNet sandbox information. Find more information here: [Cisco DevNet](https://devnetsandbox.cisco.com/). 169 | 170 | ## json-variables support 171 | 172 | At a tactical level, json-variables program lets you take a plain object (JSON files contents) and add special markers in any value which you can then reference in a different path. 173 | 174 | This library will recoginize json-variables **\*\_data** keys in the tags and delete before executing the operation. 175 | 176 | Example: 177 | 178 | ```node 179 | var lineTemplate = { 180 | pattern: "%%_extension_%%", 181 | routePartitionName: "", 182 | alertingName: "%%_firstName_%% %%_lastName_%%", 183 | asciiAlertingName: "%%_firstName_%% %%_lastName_%%", 184 | description: "%%_firstName_%% %%_lastName_%%", 185 | _data: { 186 | extension: "1001", 187 | firstName: "Tom", 188 | lastName: "Smith", 189 | }, 190 | }; 191 | 192 | const lineTags = jVar(lineTemplate); 193 | 194 | service 195 | .executeOperation("updateLine", lineTags) 196 | .then((results) => { 197 | console.log(results); 198 | }) 199 | .catch((error) => { 200 | console.log(error); 201 | }); 202 | ``` 203 | 204 | Note: If you need to change the variables key you can so via options in both the json-variables and with executeOperations. 205 | 206 | Example: 207 | 208 | ```node 209 | ... 210 | const lineTags = jVar(lineTemplate,{ dataContainerIdentifierTails: "_variables"}); 211 | 212 | service.executeOperation("updateLine", lineTags,{ dataContainerIdentifierTails: "_variables"}) 213 | ... 214 | ``` 215 | 216 | ## Limitations 217 | 218 | Currently there is an issue with strong-soap regarding returning nillable values for element tags. These values show if a particular tags is optional or not. Once resolved a method will be added to return tags nillable status (true or false). 219 | 220 | ## TypeScript Support 221 | 222 | This library includes TypeScript declarations to provide type safety and improved developer experience. 223 | 224 | ### TypeScript Usage 225 | 226 | ```typescript 227 | import axlService from 'cisco-axl'; 228 | 229 | const service = new axlService( 230 | "10.10.20.1", 231 | "administrator", 232 | "ciscopsdt", 233 | "14.0" 234 | ); 235 | 236 | async function getPartitions() { 237 | try { 238 | const operation = "listRoutePartition"; 239 | const tags = await service.getOperationTags(operation); 240 | tags.searchCriteria.name = "%%"; 241 | 242 | const result = await service.executeOperation(operation, tags); 243 | return result.routePartition; 244 | } catch (error) { 245 | console.error("Error fetching partitions:", error); 246 | throw error; 247 | } 248 | } 249 | ``` 250 | 251 | See the `examples/typescript` directory for more TypeScript examples. 252 | 253 | ## TODO 254 | 255 | - Add more promised based examples, particularly a Promise.All() example. 256 | - Add example for reading in CSV and performing a bulk exercise with variables. 257 | - Add example for saving SQL output to CSV. 258 | 259 | ## Giving Back 260 | 261 | If you would like to support my work and the time I put in creating the code, you can click the image below to get me a coffee. I would really appreciate it (but is not required). 262 | 263 | [Buy Me a Coffee](https://www.buymeacoffee.com/automatebldrs) 264 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const soap = require("strong-soap").soap; 2 | const WSDL = soap.WSDL; 3 | const path = require("path"); 4 | const https = require("https"); 5 | const { URL } = require("url"); 6 | 7 | const wsdlOptions = { 8 | attributesKey: "attributes", 9 | valueKey: "value", 10 | ns1: "ns", 11 | }; 12 | 13 | /** 14 | * Helper function to log debug messages only when DEBUG environment variable is set 15 | * @param {string} message - The message to log 16 | * @param {any} [data] - Optional data to log 17 | */ 18 | const debugLog = (message, data) => { 19 | // Get the DEBUG value, handling case-insensitivity 20 | const debug = process.env.DEBUG; 21 | 22 | // Check if DEBUG is set and is a truthy value (not 'false', 'no', '0', etc.) 23 | const isDebugEnabled = debug && !["false", "no", "0", "off", "n"].includes(debug.toLowerCase()); 24 | 25 | if (isDebugEnabled) { 26 | if (data) { 27 | console.log(`[AXL DEBUG] ${message}`, data); 28 | } else { 29 | console.log(`[AXL DEBUG] ${message}`); 30 | } 31 | } 32 | }; 33 | 34 | /** 35 | * Cisco axlService Service 36 | * This is a service class that uses fetch and promises to pull AXL data from Cisco CUCM 37 | * 38 | * @class axlService 39 | */ 40 | class axlService { 41 | constructor(host, username, password, version) { 42 | if (!host || !username || !password || !version) throw new TypeError("missing parameters"); 43 | this._OPTIONS = { 44 | username: username, 45 | password: password, 46 | url: path.join(__dirname, `/schema/${version}/AXLAPI.wsdl`), 47 | endpoint: `https://${host}:8443/axl/`, 48 | version: version, 49 | }; 50 | debugLog(`Initializing AXL service for host: ${host}, version: ${version}`); 51 | } 52 | 53 | /** 54 | * Test authentication credentials against the AXL endpoint 55 | * @returns {Promise} - Resolves to true if authentication is successful 56 | */ 57 | async testAuthentication() { 58 | try { 59 | const authSuccess = await this._testAuthenticationDirectly(); 60 | if (!authSuccess) { 61 | throw new Error("Authentication failed. Check username and password."); 62 | } 63 | return true; 64 | } catch (error) { 65 | throw new Error(`Authentication test failed: ${error.message}`); 66 | } 67 | } 68 | 69 | /** 70 | * Private method to test authentication using a simple GET request to the AXL endpoint 71 | * @returns {Promise} - Resolves with true if authentication successful, false otherwise 72 | * @private 73 | */ 74 | async _testAuthenticationDirectly() { 75 | const options = this._OPTIONS; 76 | const url = new URL(options.endpoint); 77 | 78 | return new Promise((resolve) => { 79 | const authHeader = "Basic " + Buffer.from(`${options.username}:${options.password}`).toString("base64"); 80 | 81 | const reqOptions = { 82 | hostname: url.hostname, 83 | port: url.port || 8443, 84 | path: url.pathname, 85 | method: "GET", // Simply use GET instead of POST 86 | headers: { 87 | Authorization: authHeader, 88 | Connection: "keep-alive", 89 | }, 90 | rejectUnauthorized: false, // For self-signed certificates 91 | }; 92 | 93 | debugLog(`Testing authentication to ${url.hostname}:${url.port || 8443}${url.pathname}`); 94 | 95 | const req = https.request(reqOptions, (res) => { 96 | debugLog(`Authentication test response status: ${res.statusCode}`); 97 | 98 | // Check status code for authentication failures 99 | if (res.statusCode === 401 || res.statusCode === 403) { 100 | debugLog("Authentication failed: Unauthorized status code"); 101 | resolve(false); // Authentication failed 102 | return; 103 | } 104 | 105 | let responseData = ""; 106 | 107 | res.on("data", (chunk) => { 108 | responseData += chunk; 109 | }); 110 | 111 | res.on("end", () => { 112 | // Check for the expected success message 113 | const successIndicator = "Cisco CallManager: AXL Web Service"; 114 | if (responseData.includes(successIndicator)) { 115 | debugLog("Authentication succeeded: Found success message"); 116 | resolve(true); // Authentication succeeded 117 | } else if (responseData.includes("Authentication failed") || responseData.includes("401 Unauthorized") || responseData.includes("403 Forbidden")) { 118 | debugLog("Authentication failed: Found failure message in response"); 119 | resolve(false); // Authentication failed 120 | } else { 121 | debugLog("Authentication status uncertain, response did not contain expected messages"); 122 | // If we're not sure, assume it failed to be safe 123 | resolve(false); 124 | } 125 | }); 126 | }); 127 | 128 | req.on("error", (error) => { 129 | console.error("Authentication test error:", error.message); 130 | resolve(false); 131 | }); 132 | 133 | // Since it's a GET request, we just end it without writing any data 134 | req.end(); 135 | }); 136 | } 137 | 138 | returnOperations(filter) { 139 | debugLog(`Getting available operations${filter ? ` with filter: ${filter}` : ''}`); 140 | var options = this._OPTIONS; 141 | return new Promise((resolve, reject) => { 142 | debugLog(`Creating SOAP client for ${options.url}`); 143 | soap.createClient(options.url, wsdlOptions, function (err, client) { 144 | if (err) { 145 | debugLog(`SOAP error occurred: ${err.message || 'Unknown error'}`, err); 146 | reject(err); 147 | return; 148 | } 149 | client.setSecurity(new soap.BasicAuthSecurity(options.username, options.password)); 150 | client.setEndpoint(options.endpoint); 151 | 152 | var description = client.describe(); 153 | 154 | var outputArr = []; 155 | 156 | for (const [key, value] of Object.entries(description.AXLAPIService.AXLPort)) { 157 | outputArr.push(value.name); 158 | } 159 | 160 | const sortAlphaNum = (a, b) => a.localeCompare(b, "en", { numeric: true }); 161 | const matches = (substring, array) => 162 | array.filter((element) => { 163 | if (element.toLowerCase().includes(substring.toLowerCase())) { 164 | return true; 165 | } 166 | }); 167 | 168 | if (filter) { 169 | resolve(matches(filter, outputArr).sort(sortAlphaNum)); 170 | } else { 171 | resolve(outputArr.sort(sortAlphaNum)); 172 | } 173 | 174 | client.on("soapError", function (err) { 175 | reject(err.root.Envelope.Body.Fault); 176 | }); 177 | }); 178 | }); 179 | } 180 | 181 | getOperationTags(operation) { 182 | debugLog(`Getting tags for operation: ${operation}`); 183 | var options = this._OPTIONS; 184 | return new Promise((resolve, reject) => { 185 | const wsdlPath = path.join(__dirname, `/schema/${options.version}/AXLAPI.wsdl`); 186 | debugLog(`Opening WSDL file: ${wsdlPath}`); 187 | WSDL.open(wsdlPath, wsdlOptions, function (err, wsdl) { 188 | if (err) { 189 | debugLog(`WSDL error occurred: ${err.message || 'Unknown error'}`, err); 190 | reject(err); 191 | } 192 | var operationDef = wsdl.definitions.bindings.AXLAPIBinding.operations[operation]; 193 | var operName = operationDef.$name; 194 | var operationDesc = operationDef.describe(wsdl); 195 | var envelopeBody = {}; 196 | operationDesc.input.body.elements.map((object) => { 197 | var operMatch = new RegExp(object.qname.name, "i"); 198 | envelopeBody[object.qname.name] = ""; 199 | if (object.qname.name === "searchCriteria") { 200 | let output = nestedObj(object); 201 | envelopeBody.searchCriteria = output; 202 | } 203 | if (object.qname.name === "returnedTags") { 204 | let output = nestedObj(object); 205 | envelopeBody.returnedTags = output; 206 | } 207 | if (operName.match(operMatch)) { 208 | let output = nestedObj(object); 209 | envelopeBody[object.qname.name] = output; 210 | } 211 | }); 212 | resolve(envelopeBody); 213 | }); 214 | }); 215 | } 216 | 217 | /** 218 | * Executes an AXL operation against the CUCM 219 | * @param {string} operation - The AXL operation to execute 220 | * @param {Object} tags - The tags required for the operation 221 | * @param {Object} [opts] - Optional parameters for customizing the operation 222 | * @returns {Promise} - Result of the operation 223 | */ 224 | async executeOperation(operation, tags, opts) { 225 | debugLog(`Preparing to execute operation: ${operation}`); 226 | const options = this._OPTIONS; 227 | 228 | // First test authentication 229 | debugLog(`Testing authentication before executing operation: ${operation}`); 230 | const authSuccess = await this._testAuthenticationDirectly(); 231 | if (!authSuccess) { 232 | debugLog(`Authentication failed for operation: ${operation}`); 233 | throw new Error("Authentication failed. Check username and password."); 234 | } 235 | debugLog("Authentication successful, proceeding with operation"); 236 | 237 | const clean = opts?.clean ?? false; 238 | const dataContainerIdentifierTails = opts?.dataContainerIdentifierTails ?? "_data"; 239 | const removeAttributes = opts?.removeAttributes ?? false; 240 | 241 | // Let's remove empty top level strings. Also filter out json-variables 242 | debugLog("Cleaning input tags from empty values and json-variables"); 243 | Object.keys(tags).forEach((k) => { 244 | if (tags[k] == "" || k.includes(dataContainerIdentifierTails)) { 245 | debugLog(`Removing tag: ${k}`); 246 | delete tags[k]; 247 | } 248 | }); 249 | 250 | return new Promise((resolve, reject) => { 251 | debugLog(`Creating SOAP client for operation: ${operation}`); 252 | soap.createClient(options.url, wsdlOptions, function (err, client) { 253 | if (err) { 254 | debugLog(`SOAP client creation error: ${err.message || 'Unknown error'}`, err); 255 | reject(err); 256 | return; 257 | } 258 | 259 | // Get the properly versioned namespace URL 260 | const namespaceUrl = `http://www.cisco.com/AXL/API/${options.version}`; 261 | debugLog(`Using AXL namespace: ${namespaceUrl}`); 262 | 263 | // 1. Set envelope key 264 | debugLog("Setting envelope key to 'soapenv'"); 265 | client.wsdl.options = { 266 | ...client.wsdl.options, 267 | envelopeKey: "soapenv", // Change default 'soap' to 'soapenv' 268 | }; 269 | 270 | // 2. Define namespaces with the correct version 271 | debugLog(`Setting namespace 'ns' to: ${namespaceUrl}`); 272 | client.wsdl.definitions.xmlns.ns = namespaceUrl; 273 | 274 | // Remove ns1 if it exists 275 | if (client.wsdl.definitions.xmlns.ns1) { 276 | debugLog("Removing 'ns1' namespace"); 277 | delete client.wsdl.definitions.xmlns.ns1; 278 | } 279 | 280 | var customRequestHeader = { 281 | connection: "keep-alive", 282 | SOAPAction: `"CUCM:DB ver=${options.version} ${operation}"`, 283 | }; 284 | 285 | client.setSecurity(new soap.BasicAuthSecurity(options.username, options.password)); 286 | client.setEndpoint(options.endpoint); 287 | 288 | client.on("soapError", function (err) { 289 | debugLog("SOAP error event triggered"); 290 | // Check if this is an authentication error 291 | if (err.root?.Envelope?.Body?.Fault) { 292 | const fault = err.root.Envelope.Body.Fault; 293 | const faultString = fault.faultstring || fault.faultString || ""; 294 | debugLog(`SOAP fault detected: ${faultString}`, fault); 295 | 296 | if (typeof faultString === "string" && (faultString.includes("Authentication failed") || faultString.includes("credentials") || faultString.includes("authorize"))) { 297 | debugLog("Authentication error detected in SOAP fault"); 298 | reject(new Error("Authentication failed. Check username and password.")); 299 | } else { 300 | debugLog("Non-authentication SOAP fault"); 301 | reject(fault); 302 | } 303 | } else { 304 | debugLog("Unstructured SOAP error", err); 305 | reject(err); 306 | } 307 | }); 308 | 309 | // Check if the operation function exists 310 | if (!client.AXLAPIService || !client.AXLAPIService.AXLPort || typeof client.AXLAPIService.AXLPort[operation] !== "function") { 311 | debugLog(`Operation '${operation}' not found in AXL API, attempting alternative approach`); 312 | // For operations that aren't found, try a manual approach 313 | if (operation.startsWith("apply") || operation.startsWith("reset")) { 314 | debugLog(`Using manual XML approach for ${operation} operation`); 315 | // Determine which parameter to use (name or uuid) 316 | const operationObj = tags[operation] || tags; 317 | // Check if uuid or name is provided 318 | let paramTag, paramValue; 319 | 320 | if (operationObj.uuid) { 321 | paramTag = "uuid"; 322 | paramValue = operationObj.uuid; 323 | debugLog(`Using uuid parameter: ${paramValue}`); 324 | } else { 325 | paramTag = "name"; 326 | paramValue = operationObj.name; 327 | debugLog(`Using name parameter: ${paramValue}`); 328 | } 329 | 330 | const rawXml = ` 331 | 332 | 333 | 334 | 335 | <${paramTag}>${paramValue} 336 | 337 | 338 | `; 339 | 340 | debugLog(`Executing manual XML request for operation: ${operation}`); 341 | 342 | // Use client.request for direct XML request 343 | debugLog(`Sending manual XML request to ${options.endpoint}`, { operation, paramTag, paramValue }); 344 | client._request( 345 | options.endpoint, 346 | rawXml, 347 | function (err, body, response) { 348 | if (err) { 349 | debugLog(`Error in manual XML request: ${err.message || 'Unknown error'}`, err); 350 | reject(err); 351 | return; 352 | } 353 | 354 | // Check for authentication failures in the response 355 | if (response && (response.statusCode === 401 || response.statusCode === 403)) { 356 | debugLog(`Authentication failed in manual request. Status code: ${response.statusCode}`); 357 | reject(new Error("Authentication failed. Check username and password.")); 358 | return; 359 | } 360 | 361 | if (body && typeof body === "string" && (body.includes("Authentication failed") || body.includes("401 Unauthorized") || body.includes("403 Forbidden"))) { 362 | debugLog(`Authentication failed in manual request. Found auth failure text in body.`); 363 | reject(new Error("Authentication failed. Check username and password.")); 364 | return; 365 | } 366 | 367 | debugLog(`Manual XML request response received. Size: ${body ? body.length : 0} bytes`); 368 | 369 | 370 | // Parse the response 371 | try { 372 | // Don't automatically assume success 373 | if (body && body.includes("Fault")) { 374 | debugLog("Fault detected in manual XML response"); 375 | // Try to extract the fault message 376 | const faultMatch = /(.*?)<\/faultstring>/; 377 | const match = body.match(faultMatch); 378 | if (match && match[1]) { 379 | const faultString = match[1]; 380 | debugLog(`Extracted fault string: ${faultString}`); 381 | if (faultString.includes("Authentication failed") || faultString.includes("credentials") || faultString.includes("authorize")) { 382 | debugLog("Authentication failure detected in fault string"); 383 | reject(new Error("Authentication failed. Check username and password.")); 384 | } else { 385 | debugLog(`Operation failed with fault: ${faultString}`); 386 | reject(new Error(faultString)); 387 | } 388 | } else { 389 | debugLog("Unknown SOAP fault format, couldn't extract fault string"); 390 | reject(new Error("Unknown SOAP fault occurred")); 391 | } 392 | } else { 393 | debugLog(`Operation ${operation} completed successfully via manual XML`); 394 | const result = { return: "Success" }; // Only report success if no errors found 395 | resolve(result); 396 | } 397 | } catch (parseError) { 398 | debugLog(`Error parsing manual XML response: ${parseError.message || 'Unknown error'}`, parseError); 399 | reject(parseError); 400 | } 401 | }, 402 | customRequestHeader 403 | ); 404 | 405 | return; 406 | } else { 407 | debugLog(`Operation "${operation}" not found and cannot be handled via manual XML`); 408 | reject(new Error(`Operation "${operation}" not found`)); 409 | return; 410 | } 411 | } 412 | 413 | // Get the operation function - confirmed to exist at this point 414 | var axlFunc = client.AXLAPIService.AXLPort[operation]; 415 | debugLog(`Found operation function: ${operation}`); 416 | 417 | // Define namespace context with the correct version 418 | const nsContext = { 419 | ns: namespaceUrl, 420 | }; 421 | 422 | // Prepare message for specific operations 423 | let message = tags; 424 | 425 | // Handle operations that start with "apply" or "reset" 426 | if (operation.startsWith("apply") || operation.startsWith("reset")) { 427 | debugLog(`Special message handling for ${operation} operation`); 428 | const operationKey = operation; 429 | 430 | // If there's a nested structure, flatten it 431 | if (tags[operationKey]) { 432 | debugLog(`Found nested structure for ${operationKey}`); 433 | // Check if uuid or name is provided in the nested structure 434 | if (tags[operationKey].uuid) { 435 | debugLog(`Using uuid from nested structure: ${tags[operationKey].uuid}`); 436 | message = { uuid: tags[operationKey].uuid }; 437 | } else if (tags[operationKey].name) { 438 | debugLog(`Using name from nested structure: ${tags[operationKey].name}`); 439 | message = { name: tags[operationKey].name }; 440 | } 441 | // If neither uuid nor name is provided, try to use any available 442 | else { 443 | // Try to use uuid or name from the top level as fallback 444 | if (tags.uuid) { 445 | debugLog(`Using uuid from top level: ${tags.uuid}`); 446 | message = { uuid: tags.uuid }; 447 | } else { 448 | debugLog(`Using name from top level: ${tags.name}`); 449 | message = { name: tags.name }; 450 | } 451 | } 452 | } else { 453 | debugLog(`No nested structure found for ${operationKey}, using tags directly`); 454 | } 455 | } 456 | 457 | debugLog(`Executing operation: ${operation}`); 458 | 459 | // Create a sanitized copy of the message for logging 460 | let logMessage = JSON.parse(JSON.stringify(message)); 461 | // Remove any sensitive data if present 462 | if (logMessage.password) logMessage.password = '********'; 463 | debugLog(`Preparing message for operation ${operation}:`, logMessage); 464 | 465 | // Execute the operation 466 | axlFunc( 467 | message, 468 | function (err, result, rawResponse, soapHeader, rawRequest) { 469 | if (err) { 470 | debugLog(`Error in operation ${operation}: ${err.message || 'Unknown error'}`); 471 | // Check if this is an authentication error 472 | if (err.message && (err.message.includes("Authentication failed") || err.message.includes("401 Unauthorized") || err.message.includes("403 Forbidden") || err.message.includes("credentials"))) { 473 | debugLog(`Authentication failure detected in operation error message`); 474 | reject(new Error("Authentication failed. Check username and password.")); 475 | return; 476 | } 477 | 478 | // Check if the error response indicates authentication failure 479 | if (err.response && (err.response.statusCode === 401 || err.response.statusCode === 403)) { 480 | debugLog(`Authentication failure detected in response status code: ${err.response.statusCode}`); 481 | reject(new Error("Authentication failed. Check username and password.")); 482 | return; 483 | } 484 | 485 | debugLog(`Operation ${operation} failed with error`, err); 486 | reject(err); 487 | return; 488 | } 489 | 490 | debugLog(`Operation ${operation} executed successfully`); 491 | 492 | // Check the raw response for auth failures (belt and suspenders approach) 493 | if (rawResponse && typeof rawResponse === "string" && (rawResponse.includes("Authentication failed") || rawResponse.includes("401 Unauthorized") || rawResponse.includes("403 Forbidden"))) { 494 | debugLog(`Authentication failure detected in raw response`); 495 | reject(new Error("Authentication failed. Check username and password.")); 496 | return; 497 | } 498 | 499 | if (result?.hasOwnProperty("return")) { 500 | var output = result.return; 501 | debugLog(`Operation returned data with 'return' property`); 502 | 503 | if (clean) { 504 | debugLog(`Cleaning empty/null values from output`); 505 | cleanObj(output); 506 | } 507 | if (removeAttributes) { 508 | debugLog(`Removing attribute fields from output`); 509 | cleanAttributes(output); 510 | } 511 | 512 | debugLog(`Operation ${operation} completed successfully with return data`); 513 | resolve(output); 514 | } else { 515 | debugLog(`Operation ${operation} completed successfully without return data`); 516 | resolve(result || { return: "Success" }); 517 | } 518 | }, 519 | nsContext, 520 | customRequestHeader 521 | ); 522 | }); 523 | }); 524 | } 525 | } 526 | 527 | const nestedObj = (object) => { 528 | var operObj = {}; 529 | object.elements.map((object) => { 530 | operObj[object.qname.name] = ""; 531 | if (Array.isArray(object.elements) && object.elements.length > 0) { 532 | var nestName = object.qname.name; 533 | operObj[nestName] = {}; 534 | var nestObj = nestedObj(object); 535 | operObj[nestName] = nestObj; 536 | } 537 | }); 538 | const isEmpty = Object.keys(operObj).length === 0; 539 | if (isEmpty) { 540 | operObj = ""; 541 | return operObj; 542 | } else { 543 | return operObj; 544 | } 545 | }; 546 | 547 | const cleanObj = (object) => { 548 | Object.entries(object).forEach(([k, v]) => { 549 | if (v && typeof v === "object") { 550 | cleanObj(v); 551 | } 552 | if ((v && typeof v === "object" && !Object.keys(v).length) || v === null || v === undefined) { 553 | if (Array.isArray(object)) { 554 | object.splice(k, 1); 555 | } else { 556 | delete object[k]; 557 | } 558 | } 559 | }); 560 | return object; 561 | }; 562 | 563 | const cleanAttributes = (object) => { 564 | Object.entries(object).forEach(([k, v]) => { 565 | if (v && typeof v === "object") { 566 | cleanAttributes(v); 567 | } 568 | if (v && typeof v === "object" && "attributes" in object) { 569 | if (Array.isArray(object)) { 570 | object.splice(k, 1); 571 | } else { 572 | delete object[k]; 573 | } 574 | } 575 | }); 576 | return object; 577 | }; 578 | 579 | module.exports = axlService; 580 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import * as soap from "strong-soap"; 2 | import * as path from "path"; 3 | import * as https from "https"; 4 | import { URL } from "url"; 5 | 6 | const WSDL = soap.soap.WSDL; 7 | 8 | const wsdlOptions = { 9 | attributesKey: "attributes", 10 | valueKey: "value", 11 | }; 12 | 13 | interface AXLServiceOptions { 14 | username: string; 15 | password: string; 16 | url: string; 17 | endpoint: string; 18 | version: string; 19 | } 20 | 21 | interface OperationOptions { 22 | clean?: boolean; 23 | dataContainerIdentifierTails?: string; 24 | removeAttributes?: boolean; 25 | } 26 | 27 | /** 28 | * Helper function to log debug messages only when DEBUG environment variable is set 29 | * @param {string} message - The message to log 30 | * @param {any} [data] - Optional data to log 31 | */ 32 | const debugLog = (message: string, data?: any): void => { 33 | // Get the DEBUG value, handling case-insensitivity 34 | const debug = process.env.DEBUG; 35 | 36 | // Check if DEBUG is set and is a truthy value (not 'false', 'no', '0', etc.) 37 | const isDebugEnabled = debug && !["false", "no", "0", "off", "n"].includes(debug.toLowerCase()); 38 | 39 | if (isDebugEnabled) { 40 | if (data) { 41 | console.log(`[AXL DEBUG] ${message}`, data); 42 | } else { 43 | console.log(`[AXL DEBUG] ${message}`); 44 | } 45 | } 46 | }; 47 | 48 | /** 49 | * Cisco axlService Service 50 | * This is a service class that uses fetch and promises to pull AXL data from Cisco CUCM 51 | * 52 | * @class axlService 53 | */ 54 | class axlService { 55 | private _OPTIONS: AXLServiceOptions; 56 | 57 | /** 58 | * Creates an instance of axlService. 59 | * @param {string} host - CUCM hostname or IP address 60 | * @param {string} username - CUCM username with AXL permissions 61 | * @param {string} password - CUCM password 62 | * @param {string} version - CUCM version (e.g. "14.0") 63 | * @memberof axlService 64 | */ 65 | constructor(host: string, username: string, password: string, version: string) { 66 | if (!host || !username || !password || !version) throw new TypeError("missing parameters"); 67 | this._OPTIONS = { 68 | username: username, 69 | password: password, 70 | url: path.join(__dirname, `../schema/${version}/AXLAPI.wsdl`), 71 | endpoint: `https://${host}:8443/axl/`, 72 | version: version, 73 | }; 74 | debugLog(`Initializing AXL service for host: ${host}, version: ${version}`); 75 | } 76 | 77 | /** 78 | * Test authentication credentials against the AXL endpoint 79 | * @returns {Promise} - Resolves to true if authentication is successful 80 | * @memberof axlService 81 | */ 82 | async testAuthentication(): Promise { 83 | try { 84 | const authSuccess = await this._testAuthenticationDirectly(); 85 | if (!authSuccess) { 86 | throw new Error("Authentication failed. Check username and password."); 87 | } 88 | return true; 89 | } catch (error) { 90 | throw new Error(`Authentication test failed: ${(error as Error).message}`); 91 | } 92 | } 93 | 94 | /** 95 | * Private method to test authentication using a simple GET request to the AXL endpoint 96 | * @returns {Promise} - Resolves with true if authentication successful, false otherwise 97 | * @private 98 | */ 99 | private async _testAuthenticationDirectly(): Promise { 100 | const options = this._OPTIONS; 101 | const url = new URL(options.endpoint); 102 | 103 | return new Promise((resolve) => { 104 | const authHeader = "Basic " + Buffer.from(`${options.username}:${options.password}`).toString("base64"); 105 | 106 | const reqOptions = { 107 | hostname: url.hostname, 108 | port: url.port || 8443, 109 | path: url.pathname, 110 | method: "GET", // Simply use GET instead of POST 111 | headers: { 112 | Authorization: authHeader, 113 | Connection: "keep-alive", 114 | }, 115 | rejectUnauthorized: false, // For self-signed certificates 116 | }; 117 | 118 | debugLog(`Testing authentication to ${url.hostname}:${url.port || 8443}${url.pathname}`); 119 | 120 | const req = https.request(reqOptions, (res) => { 121 | debugLog(`Authentication test response status: ${res.statusCode}`); 122 | 123 | // Check status code for authentication failures 124 | if (res.statusCode === 401 || res.statusCode === 403) { 125 | debugLog("Authentication failed: Unauthorized status code"); 126 | resolve(false); // Authentication failed 127 | return; 128 | } 129 | 130 | let responseData = ""; 131 | 132 | res.on("data", (chunk) => { 133 | responseData += chunk; 134 | }); 135 | 136 | res.on("end", () => { 137 | // Check for the expected success message 138 | const successIndicator = "Cisco CallManager: AXL Web Service"; 139 | if (responseData.includes(successIndicator)) { 140 | debugLog("Authentication succeeded: Found success message"); 141 | resolve(true); // Authentication succeeded 142 | } else if (responseData.includes("Authentication failed") || responseData.includes("401 Unauthorized") || responseData.includes("403 Forbidden")) { 143 | debugLog("Authentication failed: Found failure message in response"); 144 | resolve(false); // Authentication failed 145 | } else { 146 | debugLog("Authentication status uncertain, response did not contain expected messages"); 147 | // If we're not sure, assume it failed to be safe 148 | resolve(false); 149 | } 150 | }); 151 | }); 152 | 153 | req.on("error", (error) => { 154 | console.error("Authentication test error:", error.message); 155 | resolve(false); 156 | }); 157 | 158 | // Since it's a GET request, we just end it without writing any data 159 | req.end(); 160 | }); 161 | } 162 | 163 | /** 164 | * Returns a list of available AXL operations 165 | * @param {string} [filter] - Optional filter to narrow down operations 166 | * @returns {Promise} - Array of operation names 167 | * @memberof axlService 168 | */ 169 | returnOperations(filter?: string): Promise { 170 | debugLog(`Getting available operations${filter ? ` with filter: ${filter}` : ""}`); 171 | const options = this._OPTIONS; 172 | return new Promise((resolve, reject) => { 173 | debugLog(`Creating SOAP client for ${options.url}`); 174 | soap.soap.createClient(options.url, wsdlOptions, function (err: any, client: any) { 175 | if (err) { 176 | debugLog(`SOAP error occurred: ${err.message || "Unknown error"}`, err); 177 | reject(err); 178 | return; 179 | } 180 | 181 | client.setSecurity(new soap.soap.BasicAuthSecurity(options.username, options.password)); 182 | client.setEndpoint(options.endpoint); 183 | 184 | const description = client.describe(); 185 | const outputArr: string[] = []; 186 | 187 | for (const [, value] of Object.entries(description.AXLAPIService.AXLPort)) { 188 | outputArr.push((value as any).name); 189 | } 190 | 191 | const sortAlphaNum = (a: string, b: string) => a.localeCompare(b, "en", { numeric: true }); 192 | const matches = (substring: string, array: string[]) => 193 | array.filter((element) => { 194 | if (element.toLowerCase().includes(substring.toLowerCase())) { 195 | return true; 196 | } 197 | return false; 198 | }); 199 | 200 | if (filter) { 201 | resolve(matches(filter, outputArr).sort(sortAlphaNum)); 202 | } else { 203 | resolve(outputArr.sort(sortAlphaNum)); 204 | } 205 | 206 | client.on("soapError", function (err: any) { 207 | reject(err.root.Envelope.Body.Fault); 208 | }); 209 | }); 210 | }); 211 | } 212 | 213 | /** 214 | * Gets the tags required for a specific AXL operation 215 | * @param {string} operation - The AXL operation name 216 | * @returns {Promise} - Object containing the required tags 217 | * @memberof axlService 218 | */ 219 | getOperationTags(operation: string): Promise { 220 | debugLog(`Getting tags for operation: ${operation}`); 221 | const options = this._OPTIONS; 222 | return new Promise((resolve, reject) => { 223 | const wsdlPath = path.join(__dirname, `../schema/${options.version}/AXLAPI.wsdl`); 224 | debugLog(`Opening WSDL file: ${wsdlPath}`); 225 | WSDL.open(wsdlPath, wsdlOptions, function (err: any, wsdl: any) { 226 | if (err) { 227 | debugLog(`WSDL error occurred: ${err.message || "Unknown error"}`, err); 228 | reject(err); 229 | return; 230 | } 231 | const operationDef = wsdl.definitions.bindings.AXLAPIBinding.operations[operation]; 232 | const operName = operationDef.$name; 233 | const operationDesc = operationDef.describe(wsdl); 234 | const envelopeBody: any = {}; 235 | 236 | operationDesc.input.body.elements.map((object: any) => { 237 | const operMatch = new RegExp(object.qname.name, "i"); 238 | envelopeBody[object.qname.name] = ""; 239 | 240 | if (object.qname.name === "searchCriteria") { 241 | const output = nestedObj(object); 242 | envelopeBody.searchCriteria = output; 243 | } 244 | 245 | if (object.qname.name === "returnedTags") { 246 | const output = nestedObj(object); 247 | envelopeBody.returnedTags = output; 248 | } 249 | 250 | if (operName.match(operMatch)) { 251 | const output = nestedObj(object); 252 | envelopeBody[object.qname.name] = output; 253 | } 254 | }); 255 | 256 | resolve(envelopeBody); 257 | }); 258 | }); 259 | } 260 | 261 | /** 262 | * Executes an AXL operation against the CUCM 263 | * @param {string} operation - The AXL operation to execute 264 | * @param {any} tags - The tags required for the operation 265 | * @param {OperationOptions} [opts] - Optional parameters for customizing the operation 266 | * @returns {Promise} - Result of the operation 267 | * @memberof axlService 268 | */ 269 | async executeOperation(operation: string, tags: any, opts?: OperationOptions): Promise { 270 | debugLog(`Preparing to execute operation: ${operation}`); 271 | const options = this._OPTIONS; 272 | 273 | // First test authentication 274 | debugLog(`Testing authentication before executing operation: ${operation}`); 275 | const authSuccess = await this._testAuthenticationDirectly(); 276 | if (!authSuccess) { 277 | debugLog(`Authentication failed for operation: ${operation}`); 278 | throw new Error("Authentication failed. Check username and password."); 279 | } 280 | debugLog("Authentication successful, proceeding with operation"); 281 | 282 | const clean = opts?.clean ?? false; 283 | const dataContainerIdentifierTails = opts?.dataContainerIdentifierTails ?? "_data"; 284 | const removeAttributes = opts?.removeAttributes ?? false; 285 | 286 | // Let's remove empty top level strings. Also filter out json-variables 287 | debugLog("Cleaning input tags from empty values and json-variables"); 288 | Object.keys(tags).forEach((k) => { 289 | if (tags[k] === "" || k.includes(dataContainerIdentifierTails)) { 290 | debugLog(`Removing tag: ${k}`); 291 | delete tags[k]; 292 | } 293 | }); 294 | 295 | return new Promise((resolve, reject) => { 296 | debugLog(`Creating SOAP client for operation: ${operation}`); 297 | soap.soap.createClient(options.url, wsdlOptions, function (err: any, client: any) { 298 | if (err) { 299 | debugLog(`SOAP client creation error: ${err.message || "Unknown error"}`, err); 300 | reject(err); 301 | return; 302 | } 303 | 304 | // Get the properly versioned namespace URL 305 | const namespaceUrl = `http://www.cisco.com/AXL/API/${options.version}`; 306 | debugLog(`Using AXL namespace: ${namespaceUrl}`); 307 | 308 | // 1. Set envelope key 309 | debugLog("Setting envelope key to 'soapenv'"); 310 | client.wsdl.options = { 311 | ...client.wsdl.options, 312 | envelopeKey: "soapenv", // Change default 'soap' to 'soapenv' 313 | }; 314 | 315 | // 2. Define namespaces with the correct version 316 | debugLog(`Setting namespace 'ns' to: ${namespaceUrl}`); 317 | client.wsdl.definitions.xmlns.ns = namespaceUrl; 318 | 319 | // Remove ns1 if it exists 320 | if (client.wsdl.definitions.xmlns.ns1) { 321 | debugLog("Removing 'ns1' namespace"); 322 | delete client.wsdl.definitions.xmlns.ns1; 323 | } 324 | 325 | const customRequestHeader = { 326 | connection: "keep-alive", 327 | SOAPAction: `"CUCM:DB ver=${options.version} ${operation}"`, 328 | }; 329 | 330 | client.setSecurity(new soap.soap.BasicAuthSecurity(options.username, options.password)); 331 | client.setEndpoint(options.endpoint); 332 | 333 | client.on("soapError", function (err: any) { 334 | debugLog("SOAP error event triggered"); 335 | // Check if this is an authentication error 336 | if (err.root?.Envelope?.Body?.Fault) { 337 | const fault = err.root.Envelope.Body.Fault; 338 | const faultString = fault.faultstring || fault.faultString || ""; 339 | debugLog(`SOAP fault detected: ${faultString}`, fault); 340 | 341 | if (typeof faultString === "string" && (faultString.includes("Authentication failed") || faultString.includes("credentials") || faultString.includes("authorize"))) { 342 | debugLog("Authentication error detected in SOAP fault"); 343 | reject(new Error("Authentication failed. Check username and password.")); 344 | } else { 345 | debugLog("Non-authentication SOAP fault"); 346 | reject(fault); 347 | } 348 | } else { 349 | debugLog("Unstructured SOAP error", err); 350 | reject(err); 351 | } 352 | }); 353 | 354 | // Check if the operation function exists 355 | if (!client.AXLAPIService || !client.AXLAPIService.AXLPort || typeof client.AXLAPIService.AXLPort[operation] !== "function") { 356 | debugLog(`Operation '${operation}' not found in AXL API, attempting alternative approach`); 357 | // For operations that aren't found, try a manual approach 358 | if (operation.startsWith("apply") || operation.startsWith("reset")) { 359 | debugLog(`Using manual XML approach for ${operation} operation`); 360 | // Determine which parameter to use (name or uuid) 361 | const operationObj = tags[operation] || tags; 362 | 363 | // Check if uuid or name is provided 364 | let paramTag: string, paramValue: string; 365 | 366 | if (operationObj.uuid) { 367 | paramTag = "uuid"; 368 | paramValue = operationObj.uuid; 369 | debugLog(`Using uuid parameter: ${paramValue}`); 370 | } else { 371 | paramTag = "name"; 372 | paramValue = operationObj.name; 373 | debugLog(`Using name parameter: ${paramValue}`); 374 | } 375 | 376 | const rawXml = ` 377 | 378 | 379 | 380 | 381 | <${paramTag}>${paramValue} 382 | 383 | 384 | `; 385 | 386 | debugLog(`Executing manual XML request for operation: ${operation}`); 387 | 388 | // Use client.request for direct XML request 389 | debugLog(`Sending manual XML request to ${options.endpoint}`, { operation, paramTag, paramValue }); 390 | (client as any)._request( 391 | options.endpoint, 392 | rawXml, 393 | function (err: any, body: any, response: any) { 394 | if (err) { 395 | debugLog(`Error in manual XML request: ${err.message || "Unknown error"}`, err); 396 | reject(err); 397 | return; 398 | } 399 | 400 | // Check for authentication failures in the response 401 | if (response && (response.statusCode === 401 || response.statusCode === 403)) { 402 | debugLog(`Authentication failed in manual request. Status code: ${response.statusCode}`); 403 | reject(new Error("Authentication failed. Check username and password.")); 404 | return; 405 | } 406 | 407 | if (body && typeof body === "string" && (body.includes("Authentication failed") || body.includes("401 Unauthorized") || body.includes("403 Forbidden"))) { 408 | debugLog(`Authentication failed in manual request. Found auth failure text in body.`); 409 | reject(new Error("Authentication failed. Check username and password.")); 410 | return; 411 | } 412 | 413 | debugLog(`Manual XML request response received. Size: ${body ? body.length : 0} bytes`); 414 | 415 | // Parse the response 416 | try { 417 | // Don't automatically assume success 418 | if (body && body.includes("Fault")) { 419 | debugLog("Fault detected in manual XML response"); 420 | // Try to extract the fault message 421 | const faultMatch = /(.*?)<\/faultstring>/; 422 | const match = body.match(faultMatch); 423 | if (match && match[1]) { 424 | const faultString = match[1]; 425 | debugLog(`Extracted fault string: ${faultString}`); 426 | if (faultString.includes("Authentication failed") || faultString.includes("credentials") || faultString.includes("authorize")) { 427 | debugLog("Authentication failure detected in fault string"); 428 | reject(new Error("Authentication failed. Check username and password.")); 429 | } else { 430 | debugLog(`Operation failed with fault: ${faultString}`); 431 | reject(new Error(faultString)); 432 | } 433 | } else { 434 | debugLog("Unknown SOAP fault format, couldn't extract fault string"); 435 | reject(new Error("Unknown SOAP fault occurred")); 436 | } 437 | } else { 438 | debugLog(`Operation ${operation} completed successfully via manual XML`); 439 | const result = { return: "Success" }; // Only report success if no errors found 440 | resolve(result); 441 | } 442 | } catch (parseError) { 443 | debugLog(`Error parsing manual XML response: ${parseError instanceof Error ? parseError.message : "Unknown error"}`, parseError); 444 | reject(parseError); 445 | } 446 | }, 447 | customRequestHeader 448 | ); 449 | 450 | return; 451 | } else { 452 | debugLog(`Operation "${operation}" not found and cannot be handled via manual XML`); 453 | reject(new Error(`Operation "${operation}" not found`)); 454 | return; 455 | } 456 | } 457 | 458 | // Get the operation function - confirmed to exist at this point 459 | const axlFunc = client.AXLAPIService.AXLPort[operation]; 460 | debugLog(`Found operation function: ${operation}`); 461 | 462 | // Define namespace context with the correct version 463 | const nsContext = { 464 | ns: namespaceUrl, 465 | }; 466 | 467 | // Prepare message for specific operations 468 | let message = tags; 469 | 470 | // Handle operations that start with "apply" or "reset" 471 | if (operation.startsWith("apply") || operation.startsWith("reset")) { 472 | debugLog(`Special message handling for ${operation} operation`); 473 | const operationKey = operation; 474 | 475 | // If there's a nested structure, flatten it 476 | if (tags[operationKey]) { 477 | debugLog(`Found nested structure for ${operationKey}`); 478 | // Check if uuid or name is provided in the nested structure 479 | if (tags[operationKey].uuid) { 480 | debugLog(`Using uuid from nested structure: ${tags[operationKey].uuid}`); 481 | message = { uuid: tags[operationKey].uuid }; 482 | } else if (tags[operationKey].name) { 483 | debugLog(`Using name from nested structure: ${tags[operationKey].name}`); 484 | message = { name: tags[operationKey].name }; 485 | } 486 | // If neither uuid nor name is provided, try to use any available 487 | else { 488 | // Try to use uuid or name from the top level as fallback 489 | if (tags.uuid) { 490 | debugLog(`Using uuid from top level: ${tags.uuid}`); 491 | message = { uuid: tags.uuid }; 492 | } else { 493 | debugLog(`Using name from top level: ${tags.name}`); 494 | message = { name: tags.name }; 495 | } 496 | } 497 | } else { 498 | debugLog(`No nested structure found for ${operationKey}, using tags directly`); 499 | } 500 | } 501 | 502 | debugLog(`Executing operation: ${operation}`); 503 | 504 | // Create a sanitized copy of the message for logging 505 | let logMessage = JSON.parse(JSON.stringify(message)); 506 | // Remove any sensitive data if present 507 | if (logMessage.password) logMessage.password = "********"; 508 | debugLog(`Preparing message for operation ${operation}:`, logMessage); 509 | 510 | // Execute the operation 511 | axlFunc( 512 | message, 513 | function (err: any, result: any, rawResponse: any) { 514 | if (err) { 515 | debugLog(`Error in operation ${operation}: ${err.message || "Unknown error"}`); 516 | // Check if this is an authentication error 517 | if (err.message && (err.message.includes("Authentication failed") || err.message.includes("401 Unauthorized") || err.message.includes("403 Forbidden") || err.message.includes("credentials"))) { 518 | debugLog(`Authentication failure detected in operation error message`); 519 | reject(new Error("Authentication failed. Check username and password.")); 520 | return; 521 | } 522 | 523 | // Check if the error response indicates authentication failure 524 | if (err.response && (err.response.statusCode === 401 || err.response.statusCode === 403)) { 525 | debugLog(`Authentication failure detected in response status code: ${err.response.statusCode}`); 526 | reject(new Error("Authentication failed. Check username and password.")); 527 | return; 528 | } 529 | 530 | debugLog(`Operation ${operation} failed with error`, err); 531 | reject(err); 532 | return; 533 | } 534 | 535 | debugLog(`Operation ${operation} executed successfully`); 536 | 537 | // Check the raw response for auth failures (belt and suspenders approach) 538 | if (rawResponse && typeof rawResponse === "string" && (rawResponse.includes("Authentication failed") || rawResponse.includes("401 Unauthorized") || rawResponse.includes("403 Forbidden"))) { 539 | debugLog(`Authentication failure detected in raw response`); 540 | reject(new Error("Authentication failed. Check username and password.")); 541 | return; 542 | } 543 | 544 | if (result?.hasOwnProperty("return")) { 545 | const output = result.return; 546 | debugLog(`Operation returned data with 'return' property`); 547 | 548 | if (clean) { 549 | debugLog(`Cleaning empty/null values from output`); 550 | cleanObj(output); 551 | } 552 | if (removeAttributes) { 553 | debugLog(`Removing attribute fields from output`); 554 | cleanAttributes(output); 555 | } 556 | 557 | debugLog(`Operation ${operation} completed successfully with return data`); 558 | resolve(output); 559 | } else { 560 | debugLog(`Operation ${operation} completed successfully without return data`); 561 | resolve(result || { return: "Success" }); 562 | } 563 | }, 564 | nsContext, 565 | customRequestHeader 566 | ); 567 | }); 568 | }); 569 | } 570 | } 571 | 572 | /** 573 | * Creates a nested object from WSDL elements 574 | * @param {any} object - The WSDL object to process 575 | * @returns {any} - Processed object 576 | */ 577 | const nestedObj = (object: any): any => { 578 | const operObj: any = {}; 579 | 580 | object.elements.map((object: any) => { 581 | operObj[object.qname.name] = ""; 582 | 583 | if (Array.isArray(object.elements) && object.elements.length > 0) { 584 | const nestName = object.qname.name; 585 | operObj[nestName] = {}; 586 | const nestObj = nestedObj(object); 587 | operObj[nestName] = nestObj; 588 | } 589 | }); 590 | 591 | const isEmpty = Object.keys(operObj).length === 0; 592 | 593 | if (isEmpty) { 594 | return ""; 595 | } else { 596 | return operObj; 597 | } 598 | }; 599 | 600 | /** 601 | * Cleans an object by removing null, undefined, or empty values 602 | * @param {any} object - The object to clean 603 | * @returns {any} - Cleaned object 604 | */ 605 | const cleanObj = (object: any): any => { 606 | Object.entries(object).forEach(([k, v]) => { 607 | if (v && typeof v === "object") { 608 | cleanObj(v); 609 | } 610 | 611 | if ((v && typeof v === "object" && !Object.keys(v).length) || v === null || v === undefined) { 612 | if (Array.isArray(object)) { 613 | object.splice(parseInt(k), 1); 614 | } else { 615 | delete object[k]; 616 | } 617 | } 618 | }); 619 | 620 | return object; 621 | }; 622 | 623 | /** 624 | * Removes attribute fields from an object 625 | * @param {any} object - The object to clean 626 | * @returns {any} - Cleaned object 627 | */ 628 | const cleanAttributes = (object: any): any => { 629 | Object.entries(object).forEach(([k, v]) => { 630 | if (v && typeof v === "object") { 631 | cleanAttributes(v); 632 | } 633 | 634 | if (v && typeof v === "object" && "attributes" in object) { 635 | if (Array.isArray(object)) { 636 | object.splice(parseInt(k), 1); 637 | } else { 638 | delete object[k]; 639 | } 640 | } 641 | }); 642 | 643 | return object; 644 | }; 645 | 646 | export = axlService; 647 | --------------------------------------------------------------------------------