From 8ed19c0657279eddb84b3b49b240c3725cd376a5 Mon Sep 17 00:00:00 2001 From: Sreekanth Narayanan <131740035+sreenara@users.noreply.github.com> Date: Thu, 5 Sep 2024 09:01:54 +0530 Subject: [PATCH 1/5] docs(byods): added initial readme file for byods (#3810) --- packages/byods/README.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 packages/byods/README.md diff --git a/packages/byods/README.md b/packages/byods/README.md new file mode 100644 index 00000000000..7fb7c61ac3e --- /dev/null +++ b/packages/byods/README.md @@ -0,0 +1,3 @@ +# BYoDS SDK + +This is the readme placeholder for the Bring Your own Data Source (BYoDS) SDK. \ No newline at end of file From 97d89360d65c6494f60e69d29cf11d2210a27b66 Mon Sep 17 00:00:00 2001 From: Bharath Balan <62698609+bhabalan@users.noreply.github.com> Date: Thu, 5 Sep 2024 21:56:26 +0530 Subject: [PATCH 2/5] feat(byods): project setup (#3811) --- package.json | 1 + packages/byods/.gitignore | 126 +++++++++++++++++++++++++++++ packages/byods/babel.config.json | 13 +++ packages/byods/package.json | 120 ++++++++++++++++++++++++++++ packages/byods/src/BYODS.ts | 47 +++++++++++ packages/byods/src/apiClient.ts | 28 +++++++ packages/byods/src/index.ts | 1 + packages/byods/tsconfig.json | 131 +++++++++++++++++++++++++++++++ yarn.lock | 73 +++++++++++++++++ 9 files changed, 540 insertions(+) create mode 100644 packages/byods/.gitignore create mode 100644 packages/byods/babel.config.json create mode 100644 packages/byods/package.json create mode 100644 packages/byods/src/BYODS.ts create mode 100644 packages/byods/src/apiClient.ts create mode 100644 packages/byods/src/index.ts create mode 100644 packages/byods/tsconfig.json diff --git a/package.json b/package.json index 775d5e4f22a..b2a031db50c 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "./packages/@webex/*", "./packages/webex", "./packages/calling", + "./packages/byods", "./packages/config/*", "./packages/legacy/*", "./packages/tools/*" diff --git a/packages/byods/.gitignore b/packages/byods/.gitignore new file mode 100644 index 00000000000..a7ec2157442 --- /dev/null +++ b/packages/byods/.gitignore @@ -0,0 +1,126 @@ +# Webstorm +.idea + +# Other +.DS_Store + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov +coverage +junit.xml + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test +.env.production + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +docs/temp +*.xml + +samples/index.min.js + +# End of https://www.toptal.com/developers/gitignore/api/node diff --git a/packages/byods/babel.config.json b/packages/byods/babel.config.json new file mode 100644 index 00000000000..e5ed0dab0e9 --- /dev/null +++ b/packages/byods/babel.config.json @@ -0,0 +1,13 @@ +{ + "presets": [ + [ + "@babel/preset-env", + { + "targets": { + "node": "current" + } + } + ], + "@babel/preset-typescript" + ], +} diff --git a/packages/byods/package.json b/packages/byods/package.json new file mode 100644 index 00000000000..8e79ed9c7a6 --- /dev/null +++ b/packages/byods/package.json @@ -0,0 +1,120 @@ +{ + "name": "@webex/byods", + "files": [ + "dist" + ], + "contributors": [], + "main": "dist/module/index.js", + "module": "dist/module/index.js", + "types": "dist/types/index.d.ts", + "license": "MIT", + "author": "devsupport@webex.com", + "repository": { + "type": "git", + "url": "https://github.com/webex/webex-js-sdk.git", + "directory": "packages/byods" + }, + "engines": { + "node": ">=20.x" + }, + "scripts": { + "build:docs": "typedoc --out ../../docs/byods", + "build:src": "webex-legacy-tools build -dest \"./dist\" -src \"./src\" -js -ts && yarn build", + "build": "yarn run -T tsc --declaration true --declarationDir ./dist/types", + "deploy:npm": "yarn npm publish", + "docs": "typedoc --emit none", + "fix:lint": "eslint 'src/**/*.ts' --fix", + "fix:prettier": "prettier \"src/**/*.ts\" --write", + "prebuild": "rimraf dist" + }, + "devDependencies": { + "@babel/preset-typescript": "7.16.7", + "@commitlint/cli": "15.0.0", + "@commitlint/config-conventional": "15.0.0", + "@rollup/plugin-commonjs": "22.0.2", + "@rollup/plugin-json": "4.1.0", + "@rollup/plugin-node-resolve": "13.1.3", + "@types/chai": "4.2.21", + "@types/jest": "27.4.1", + "@types/mocha": "9.0.0", + "@types/node": "16.11.9", + "@types/uuid": "8.3.4", + "@typescript-eslint/eslint-plugin": "5.38.1", + "@typescript-eslint/parser": "5.38.1", + "@web/dev-server": "0.4.5", + "chai": "4.3.4", + "cspell": "5.19.2", + "esbuild": "^0.17.19", + "eslint": "^8.24.0", + "eslint-config-airbnb-base": "15.0.0", + "eslint-config-prettier": "8.3.0", + "eslint-import-resolver-typescript": "2.4.0", + "eslint-plugin-import": "2.25.3", + "eslint-plugin-jsdoc": "38.0.4", + "eslint-plugin-prettier": "4.0.0", + "eslint-plugin-tsdoc": "0.2.14", + "jest": "27.5.1", + "jest-junit": "13.0.0", + "karma": "6.4.3", + "karma-chai": "0.1.0", + "karma-chrome-launcher": "3.1.0", + "karma-coverage": "2.0.3", + "karma-firefox-launcher": "2.1.1", + "karma-junit-reporter": "2.0.1", + "karma-mocha": "2.0.1", + "karma-mocha-reporter": "2.2.5", + "karma-safari-launcher": "1.0.0", + "karma-sauce-launcher": "4.3.6", + "karma-typescript": "5.5.3", + "karma-typescript-es6-transform": "5.5.3", + "mocha": "10.6.0", + "prettier": "2.5.1", + "puppeteer": "22.13.0", + "rimraf": "3.0.2", + "rollup": "2.68.0", + "rollup-plugin-polyfill-node": "0.8.0", + "rollup-plugin-terser": "7.0.2", + "rollup-plugin-typescript2": "0.31.2", + "sinon": "12.0.1", + "ts-jest": "27.1.4", + "typed-emitter": "2.1.0", + "typedoc": "0.23.26", + "typescript": "4.9.5" + }, + "commitlint": { + "extends": [ + "@commitlint/config-conventional" + ], + "rules": { + "scope-case": [ + 2, + "always", + [ + "lower-case", + "pascal-case" + ] + ], + "body-max-line-length": [ + 0, + "always", + 400 + ], + "footer-max-line-length": [ + 0, + "always", + 400 + ] + } + }, + "gh-pages-deploy": { + "staticpath": "docs", + "noprompt": true + }, + "dependencies": { + "@types/node-fetch": "^2.6.11", + "@webex/legacy-tools": "workspace:*", + "@webex/media-helpers": "workspace:*", + "node-fetch": "^3.3.2" + }, + "type": "module" +} diff --git a/packages/byods/src/BYODS.ts b/packages/byods/src/BYODS.ts new file mode 100644 index 00000000000..5b19a07f3d3 --- /dev/null +++ b/packages/byods/src/BYODS.ts @@ -0,0 +1,47 @@ +/* eslint-disable no-console */ +import fetch from 'node-fetch'; +import {LocalStream} from '@webex/media-helpers'; // Just to show that you can import other packages from the workspace + +interface SDKConfig { + clientId: string; + clientSecret: string; + accessToken: string; + refreshToken: string; + expiresAt: Date; +} + +class BYODS { + private clientId: string; + private clientSecret: string; + private tokenHost: string; + private accessToken: string; + private refreshToken: string; + private expiresAt: Date; + + constructor({clientId, clientSecret, accessToken, refreshToken, expiresAt}: SDKConfig) { + this.clientId = clientId; + this.clientSecret = clientSecret; + this.accessToken = accessToken; + this.refreshToken = refreshToken; + this.expiresAt = expiresAt; + this.tokenHost = 'https://webexapis.com/v1/access_token'; + } + + public async makeAuthenticatedRequest(endpoint: string): Promise { + if (new Date() >= new Date(this.expiresAt as Date)) { + throw new Error('Token has expired'); + } + + // Use this.token.access_token to make authenticated requests + const response = await fetch(endpoint, { + headers: { + Authorization: `Bearer ${this.accessToken}`, + }, + }); + + return response.json(); + } +} + +export default BYODS; +export {SDKConfig, LocalStream}; diff --git a/packages/byods/src/apiClient.ts b/packages/byods/src/apiClient.ts new file mode 100644 index 00000000000..282b5126342 --- /dev/null +++ b/packages/byods/src/apiClient.ts @@ -0,0 +1,28 @@ +import BYODS, {SDKConfig} from './BYODS'; + +const config: SDKConfig = { + clientId: 'your-client-id', + clientSecret: 'your-client-secret', + accessToken: 'your-initial-access-token', + refreshToken: 'your-refresh-token', + expiresAt: new Date('2024-09-15T00:00:00Z'), +}; +const sdk = new BYODS(config); + +// This function is just a placeholder to test project setup. +async function listDataSources() { + await sdk.makeAuthenticatedRequest( + 'https://developer-applications.ciscospark.com/v1/dataSources/' + ); +} + +// This function is just a placeholder to test project setup. +async function getDataSources(id: string) { + await sdk.makeAuthenticatedRequest( + `https://developer-applications.ciscospark.com/v1/dataSources/${id}` + ); +} + +export {listDataSources, getDataSources}; + +export default listDataSources; diff --git a/packages/byods/src/index.ts b/packages/byods/src/index.ts new file mode 100644 index 00000000000..158408b0be9 --- /dev/null +++ b/packages/byods/src/index.ts @@ -0,0 +1 @@ +export {listDataSources, getDataSources} from './apiClient'; diff --git a/packages/byods/tsconfig.json b/packages/byods/tsconfig.json new file mode 100644 index 00000000000..95a8783c6e1 --- /dev/null +++ b/packages/byods/tsconfig.json @@ -0,0 +1,131 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "ESNext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + "lib": [ + "ES2018.Promise", + "ES2021.String" + ], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "NodeNext", /* Specify what module code is generated. */ + "rootDir": "src", /* Specify the root folder within your source files. */ + "moduleResolution": "Node", /* Specify how TypeScript looks up a file from a given module specifier. */ + "baseUrl": "./src", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + "outDir": "dist/module", /* Specify an output folder for all emitted files. */ + "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + "declarationDir": "dist/types", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + "suppressImplicitAnyIndexErrors":true /* Suppresses errors about implicit anys when indexing into objects. */, + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + }, + "typedocOptions": { + "entryPoints": [ + "./src/index.ts" + ], + "sort": [ + "source-order" + ], + "name": "BYoDS SDK (@webex/byods)", + "disableSources": "true", + "entryPointStrategy": "expand", + "excludeExternals": "true", + "excludePrivate": "true", + "hideGenerator": "true", + }, + "include": [ + "src/**/*.ts", + "jest.global.d.ts" + ], + "exclude": [ + "./node_modules/**", + "./test", + "./docs/examples", + "./src/**/*.test.ts" + ] +} diff --git a/yarn.lock b/yarn.lock index e2d039cfc0c..b9fe145a919 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5861,6 +5861,16 @@ __metadata: languageName: node linkType: hard +"@types/node-fetch@npm:^2.6.11": + version: 2.6.11 + resolution: "@types/node-fetch@npm:2.6.11" + dependencies: + "@types/node": "*" + form-data: ^4.0.0 + checksum: 180e4d44c432839bdf8a25251ef8c47d51e37355ddd78c64695225de8bc5dc2b50b7bb855956d471c026bb84bd7295688a0960085e7158cbbba803053492568b + languageName: node + linkType: hard + "@types/node-forge@npm:^1.3.0": version: 1.3.10 resolution: "@types/node-forge@npm:1.3.10" @@ -7400,6 +7410,69 @@ __metadata: languageName: unknown linkType: soft +"@webex/byods@workspace:packages/byods": + version: 0.0.0-use.local + resolution: "@webex/byods@workspace:packages/byods" + dependencies: + "@babel/preset-typescript": 7.16.7 + "@commitlint/cli": 15.0.0 + "@commitlint/config-conventional": 15.0.0 + "@rollup/plugin-commonjs": 22.0.2 + "@rollup/plugin-json": 4.1.0 + "@rollup/plugin-node-resolve": 13.1.3 + "@types/chai": 4.2.21 + "@types/jest": 27.4.1 + "@types/mocha": 9.0.0 + "@types/node": 16.11.9 + "@types/node-fetch": ^2.6.11 + "@types/uuid": 8.3.4 + "@typescript-eslint/eslint-plugin": 5.38.1 + "@typescript-eslint/parser": 5.38.1 + "@web/dev-server": 0.4.5 + "@webex/legacy-tools": "workspace:*" + "@webex/media-helpers": "workspace:*" + chai: 4.3.4 + cspell: 5.19.2 + esbuild: ^0.17.19 + eslint: ^8.24.0 + eslint-config-airbnb-base: 15.0.0 + eslint-config-prettier: 8.3.0 + eslint-import-resolver-typescript: 2.4.0 + eslint-plugin-import: 2.25.3 + eslint-plugin-jsdoc: 38.0.4 + eslint-plugin-prettier: 4.0.0 + eslint-plugin-tsdoc: 0.2.14 + jest: 27.5.1 + jest-junit: 13.0.0 + karma: 6.4.3 + karma-chai: 0.1.0 + karma-chrome-launcher: 3.1.0 + karma-coverage: 2.0.3 + karma-firefox-launcher: 2.1.1 + karma-junit-reporter: 2.0.1 + karma-mocha: 2.0.1 + karma-mocha-reporter: 2.2.5 + karma-safari-launcher: 1.0.0 + karma-sauce-launcher: 4.3.6 + karma-typescript: 5.5.3 + karma-typescript-es6-transform: 5.5.3 + mocha: 10.6.0 + node-fetch: ^3.3.2 + prettier: 2.5.1 + puppeteer: 22.13.0 + rimraf: 3.0.2 + rollup: 2.68.0 + rollup-plugin-polyfill-node: 0.8.0 + rollup-plugin-terser: 7.0.2 + rollup-plugin-typescript2: 0.31.2 + sinon: 12.0.1 + ts-jest: 27.1.4 + typed-emitter: 2.1.0 + typedoc: 0.23.26 + typescript: 4.9.5 + languageName: unknown + linkType: soft + "@webex/calling@workspace:*, @webex/calling@workspace:packages/calling": version: 0.0.0-use.local resolution: "@webex/calling@workspace:packages/calling" From 0c1a7585b0a937936f6b7561313dda1eb970fcf1 Mon Sep 17 00:00:00 2001 From: Kesari3008 <65543166+Kesari3008@users.noreply.github.com> Date: Thu, 12 Sep 2024 10:23:33 +0530 Subject: [PATCH 3/5] feat(byods): Spark 549034 jest setup (#3821) --- packages/byods/jest.config.js | 47 +++++++++++++++++++ packages/byods/jest.d.ts | 10 ++++ packages/byods/jest.setup.js | 12 +++++ packages/byods/package.json | 6 ++- packages/byods/src/BYODS.ts | 3 +- packages/byods/src/BYoDS.test.ts | 32 +++++++++++++ .../src/models/package/package.constants.ts | 2 + .../tools/src/models/package/package.ts | 14 ++++-- yarn.lock | 1 + 9 files changed, 121 insertions(+), 6 deletions(-) create mode 100644 packages/byods/jest.config.js create mode 100644 packages/byods/jest.d.ts create mode 100644 packages/byods/jest.setup.js create mode 100644 packages/byods/src/BYoDS.test.ts diff --git a/packages/byods/jest.config.js b/packages/byods/jest.config.js new file mode 100644 index 00000000000..70be0abdfce --- /dev/null +++ b/packages/byods/jest.config.js @@ -0,0 +1,47 @@ +import config from '@webex/jest-config-legacy'; + +const jestConfig = { + rootDir: './', + setupFilesAfterEnv: ['/jest.setup.js'], + testEnvironment: 'node', + testMatch: ['/**/*.test.ts'], + transformIgnorePatterns: ['/node_modules/(?!node-fetch)|data-uri-to-buffer'], + testPathIgnorePatterns: ['/node_modules/', '/dist/'], + testResultsProcessor: 'jest-junit', + // Clear mocks in between tests by default + clearMocks: true, + // TODO: Set this to true once we have the source code and their corresponding test files added + collectCoverage: false, + coverageThreshold: { + global: { + lines: 85, + functions: 85, + branches: 85, + statements: 85, + }, + }, + coverageDirectory: 'coverage', + coverageReporters: ['clover', 'json', 'lcov'], + reporters: [ + 'default', + [ + 'jest-junit', + { + outputDirectory: 'coverage/junit', + outputName: 'coverage-junit.xml', + classNameTemplate: '{classname}', + titleTemplate: '{title}', + }, + ], + [ + 'jest-html-reporters', + { + publicPath: './coverage', + filename: 'jest-report.html', + openReport: false, + }, + ], + ], +}; + +export default {...config, ...jestConfig}; diff --git a/packages/byods/jest.d.ts b/packages/byods/jest.d.ts new file mode 100644 index 00000000000..22494008fb9 --- /dev/null +++ b/packages/byods/jest.d.ts @@ -0,0 +1,10 @@ +import 'jest'; + +declare global { + namespace jest { + /* eslint-disable-next-line no-unused-vars */ + interface Matchers { + toBeCalledOnceWith(received?: any, ...expected: any[]): CustomMatcherResult; + } + } +} diff --git a/packages/byods/jest.setup.js b/packages/byods/jest.setup.js new file mode 100644 index 00000000000..2f53d161070 --- /dev/null +++ b/packages/byods/jest.setup.js @@ -0,0 +1,12 @@ +expect.extend({ + toBeCalledOnceWith(received, ...expected) { + try { + expect(received).toBeCalledTimes(1); + expect(received).toBeCalledWith(...expected); + } catch (error) { + return {message: () => error, pass: false}; + } + + return {pass: true}; + }, +}); \ No newline at end of file diff --git a/packages/byods/package.json b/packages/byods/package.json index 8e79ed9c7a6..1d2567c3ce5 100644 --- a/packages/byods/package.json +++ b/packages/byods/package.json @@ -25,7 +25,10 @@ "docs": "typedoc --emit none", "fix:lint": "eslint 'src/**/*.ts' --fix", "fix:prettier": "prettier \"src/**/*.ts\" --write", - "prebuild": "rimraf dist" + "prebuild": "rimraf dist", + "test": "yarn test:style && yarn test:unit", + "test:style": "eslint 'src/**/*.ts'", + "test:unit": "webex-legacy-tools test --unit --runner jest" }, "devDependencies": { "@babel/preset-typescript": "7.16.7", @@ -112,6 +115,7 @@ }, "dependencies": { "@types/node-fetch": "^2.6.11", + "@webex/jest-config-legacy": "workspace:*", "@webex/legacy-tools": "workspace:*", "@webex/media-helpers": "workspace:*", "node-fetch": "^3.3.2" diff --git a/packages/byods/src/BYODS.ts b/packages/byods/src/BYODS.ts index 5b19a07f3d3..1a3d25ff304 100644 --- a/packages/byods/src/BYODS.ts +++ b/packages/byods/src/BYODS.ts @@ -1,6 +1,5 @@ /* eslint-disable no-console */ import fetch from 'node-fetch'; -import {LocalStream} from '@webex/media-helpers'; // Just to show that you can import other packages from the workspace interface SDKConfig { clientId: string; @@ -44,4 +43,4 @@ class BYODS { } export default BYODS; -export {SDKConfig, LocalStream}; +export {SDKConfig}; diff --git a/packages/byods/src/BYoDS.test.ts b/packages/byods/src/BYoDS.test.ts new file mode 100644 index 00000000000..a46208c481f --- /dev/null +++ b/packages/byods/src/BYoDS.test.ts @@ -0,0 +1,32 @@ +import fetch, {Response} from 'node-fetch'; +import BYODS from './BYODS'; + +jest.mock('node-fetch', () => jest.fn()); + +describe('BYoDS Tests', () => { + const mockSDKConfig = { + clientId: 'your-client-id', + clientSecret: 'your-client-secret', + accessToken: 'your-initial-access-token', + refreshToken: 'your-refresh-token', + expiresAt: new Date('2024-09-15T00:00:00Z'), + }; + + const mockResponse = { + json: jest.fn().mockResolvedValue({key: 'value'}), + } as unknown as Response; + + (fetch as unknown as jest.MockedFunction).mockResolvedValue(mockResponse); + + it('fetch the datasources', async () => { + const mockPayload = { + headers: { + Authorization: `Bearer ${mockSDKConfig.accessToken}`, + }, + }; + const sdk = new BYODS(mockSDKConfig); + const endpoint = 'https://developer-applications.ciscospark.com/v1/dataSources/'; + await sdk.makeAuthenticatedRequest(endpoint); + expect(fetch).toHaveBeenCalledWith(endpoint, mockPayload); + }); +}); diff --git a/packages/legacy/tools/src/models/package/package.constants.ts b/packages/legacy/tools/src/models/package/package.constants.ts index dc6ba980717..de4f9e9a115 100644 --- a/packages/legacy/tools/src/models/package/package.constants.ts +++ b/packages/legacy/tools/src/models/package/package.constants.ts @@ -5,6 +5,7 @@ const PATTERNS = { JAVASCRIPT: './**/*.js', TYPESCRIPT: './**/*.ts', TEST: './**/*.*', + BYODS: './*.test.ts', }; /** @@ -14,6 +15,7 @@ const TEST_DIRECTORIES = { INTEGRATION: './integration/spec', ROOT: './test', UNIT: './unit/spec', + SRC: './src', }; const CONSTANTS = { diff --git a/packages/legacy/tools/src/models/package/package.ts b/packages/legacy/tools/src/models/package/package.ts index c336e4e0ed0..833937dbbac 100644 --- a/packages/legacy/tools/src/models/package/package.ts +++ b/packages/legacy/tools/src/models/package/package.ts @@ -100,6 +100,14 @@ class Package { public test(config: TestConfig): Promise { const testDirectory = path.join(this.data.packageRoot, CONSTANTS.TEST_DIRECTORIES.ROOT); + const unitTestFileCollectorInSrc = config.unit + ? Package.getFiles({ + location: path.join(this.data.packageRoot, CONSTANTS.TEST_DIRECTORIES.SRC), + pattern: CONSTANTS.PATTERNS.BYODS, + targets: config.targets, + }) + : Promise.resolve([]); + const unitTestFileCollector = config.unit ? Package.getFiles({ location: path.join(testDirectory, CONSTANTS.TEST_DIRECTORIES.UNIT), @@ -116,10 +124,10 @@ class Package { }) : Promise.resolve([]); - return Promise.all([unitTestFileCollector, integrationTestFileCollector]) - .then(async ([unitFiles, integrationFiles]) => { + return Promise.all([unitTestFileCollector, integrationTestFileCollector, unitTestFileCollectorInSrc]) + .then(async ([unitFiles, integrationFiles, srcUnitFiles]) => { if (config.runner === 'jest') { - const testFiles = [...unitFiles]; + const testFiles = [...unitFiles, ...srcUnitFiles]; if (testFiles.length > 0) { await Jest.test({ files: testFiles }); diff --git a/yarn.lock b/yarn.lock index b9fe145a919..58596af11ff 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7429,6 +7429,7 @@ __metadata: "@typescript-eslint/eslint-plugin": 5.38.1 "@typescript-eslint/parser": 5.38.1 "@web/dev-server": 0.4.5 + "@webex/jest-config-legacy": "workspace:*" "@webex/legacy-tools": "workspace:*" "@webex/media-helpers": "workspace:*" chai: 4.3.4 From 8f30732fe0fbf429b1ace9add6f3d77d5798a5c4 Mon Sep 17 00:00:00 2001 From: Bharath Balan <62698609+bhabalan@users.noreply.github.com> Date: Wed, 18 Sep 2024 12:30:35 +0530 Subject: [PATCH 4/5] feat(byods): create the main BYODS class implementation (#3824) --- packages/byods/jest.config.js | 4 +- packages/byods/package.json | 10 +- packages/byods/src/BYODS.ts | 46 ---- packages/byods/src/BYoDS.test.ts | 32 --- packages/byods/src/apiClient.ts | 28 --- packages/byods/src/base-client/index.ts | 201 +++++++++++++++++ packages/byods/src/byods/index.ts | 91 ++++++++ packages/byods/src/constants.ts | 10 + .../byods/src/data-source-client/constants.ts | 1 + .../byods/src/data-source-client/index.ts | 89 ++++++++ .../byods/src/data-source-client/types.ts | 98 +++++++++ packages/byods/src/http-client/types.ts | 59 +++++ packages/byods/src/index.ts | 7 +- packages/byods/src/token-manager/index.ts | 186 ++++++++++++++++ packages/byods/src/types.ts | 104 +++++++++ .../byods/test/unit/spec/base-client/index.ts | 70 ++++++ packages/byods/test/unit/spec/byods/index.ts | 32 +++ .../unit/spec/data-source-client/index.ts | 204 ++++++++++++++++++ .../test/unit/spec/token-manager/index.ts | 144 +++++++++++++ packages/byods/tsconfig.json | 5 +- yarn.lock | 37 ++-- 21 files changed, 1327 insertions(+), 131 deletions(-) delete mode 100644 packages/byods/src/BYODS.ts delete mode 100644 packages/byods/src/BYoDS.test.ts delete mode 100644 packages/byods/src/apiClient.ts create mode 100644 packages/byods/src/base-client/index.ts create mode 100644 packages/byods/src/byods/index.ts create mode 100644 packages/byods/src/constants.ts create mode 100644 packages/byods/src/data-source-client/constants.ts create mode 100644 packages/byods/src/data-source-client/index.ts create mode 100644 packages/byods/src/data-source-client/types.ts create mode 100644 packages/byods/src/http-client/types.ts create mode 100644 packages/byods/src/token-manager/index.ts create mode 100644 packages/byods/src/types.ts create mode 100644 packages/byods/test/unit/spec/base-client/index.ts create mode 100644 packages/byods/test/unit/spec/byods/index.ts create mode 100644 packages/byods/test/unit/spec/data-source-client/index.ts create mode 100644 packages/byods/test/unit/spec/token-manager/index.ts diff --git a/packages/byods/jest.config.js b/packages/byods/jest.config.js index 70be0abdfce..7a63b38ae69 100644 --- a/packages/byods/jest.config.js +++ b/packages/byods/jest.config.js @@ -4,8 +4,8 @@ const jestConfig = { rootDir: './', setupFilesAfterEnv: ['/jest.setup.js'], testEnvironment: 'node', - testMatch: ['/**/*.test.ts'], - transformIgnorePatterns: ['/node_modules/(?!node-fetch)|data-uri-to-buffer'], + testMatch: ['/test/unit/spec/**/*.ts'], + transformIgnorePatterns: [], testPathIgnorePatterns: ['/node_modules/', '/dist/'], testResultsProcessor: 'jest-junit', // Clear mocks in between tests by default diff --git a/packages/byods/package.json b/packages/byods/package.json index 1d2567c3ce5..e5c09d224fe 100644 --- a/packages/byods/package.json +++ b/packages/byods/package.json @@ -45,6 +45,8 @@ "@typescript-eslint/eslint-plugin": "5.38.1", "@typescript-eslint/parser": "5.38.1", "@web/dev-server": "0.4.5", + "@webex/jest-config-legacy": "workspace:*", + "@webex/legacy-tools": "workspace:*", "chai": "4.3.4", "cspell": "5.19.2", "esbuild": "^0.17.19", @@ -114,11 +116,9 @@ "noprompt": true }, "dependencies": { - "@types/node-fetch": "^2.6.11", - "@webex/jest-config-legacy": "workspace:*", - "@webex/legacy-tools": "workspace:*", - "@webex/media-helpers": "workspace:*", - "node-fetch": "^3.3.2" + "@types/node-fetch": "2.6.11", + "jose": "5.8.0", + "node-fetch": "3.3.2" }, "type": "module" } diff --git a/packages/byods/src/BYODS.ts b/packages/byods/src/BYODS.ts deleted file mode 100644 index 1a3d25ff304..00000000000 --- a/packages/byods/src/BYODS.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* eslint-disable no-console */ -import fetch from 'node-fetch'; - -interface SDKConfig { - clientId: string; - clientSecret: string; - accessToken: string; - refreshToken: string; - expiresAt: Date; -} - -class BYODS { - private clientId: string; - private clientSecret: string; - private tokenHost: string; - private accessToken: string; - private refreshToken: string; - private expiresAt: Date; - - constructor({clientId, clientSecret, accessToken, refreshToken, expiresAt}: SDKConfig) { - this.clientId = clientId; - this.clientSecret = clientSecret; - this.accessToken = accessToken; - this.refreshToken = refreshToken; - this.expiresAt = expiresAt; - this.tokenHost = 'https://webexapis.com/v1/access_token'; - } - - public async makeAuthenticatedRequest(endpoint: string): Promise { - if (new Date() >= new Date(this.expiresAt as Date)) { - throw new Error('Token has expired'); - } - - // Use this.token.access_token to make authenticated requests - const response = await fetch(endpoint, { - headers: { - Authorization: `Bearer ${this.accessToken}`, - }, - }); - - return response.json(); - } -} - -export default BYODS; -export {SDKConfig}; diff --git a/packages/byods/src/BYoDS.test.ts b/packages/byods/src/BYoDS.test.ts deleted file mode 100644 index a46208c481f..00000000000 --- a/packages/byods/src/BYoDS.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -import fetch, {Response} from 'node-fetch'; -import BYODS from './BYODS'; - -jest.mock('node-fetch', () => jest.fn()); - -describe('BYoDS Tests', () => { - const mockSDKConfig = { - clientId: 'your-client-id', - clientSecret: 'your-client-secret', - accessToken: 'your-initial-access-token', - refreshToken: 'your-refresh-token', - expiresAt: new Date('2024-09-15T00:00:00Z'), - }; - - const mockResponse = { - json: jest.fn().mockResolvedValue({key: 'value'}), - } as unknown as Response; - - (fetch as unknown as jest.MockedFunction).mockResolvedValue(mockResponse); - - it('fetch the datasources', async () => { - const mockPayload = { - headers: { - Authorization: `Bearer ${mockSDKConfig.accessToken}`, - }, - }; - const sdk = new BYODS(mockSDKConfig); - const endpoint = 'https://developer-applications.ciscospark.com/v1/dataSources/'; - await sdk.makeAuthenticatedRequest(endpoint); - expect(fetch).toHaveBeenCalledWith(endpoint, mockPayload); - }); -}); diff --git a/packages/byods/src/apiClient.ts b/packages/byods/src/apiClient.ts deleted file mode 100644 index 282b5126342..00000000000 --- a/packages/byods/src/apiClient.ts +++ /dev/null @@ -1,28 +0,0 @@ -import BYODS, {SDKConfig} from './BYODS'; - -const config: SDKConfig = { - clientId: 'your-client-id', - clientSecret: 'your-client-secret', - accessToken: 'your-initial-access-token', - refreshToken: 'your-refresh-token', - expiresAt: new Date('2024-09-15T00:00:00Z'), -}; -const sdk = new BYODS(config); - -// This function is just a placeholder to test project setup. -async function listDataSources() { - await sdk.makeAuthenticatedRequest( - 'https://developer-applications.ciscospark.com/v1/dataSources/' - ); -} - -// This function is just a placeholder to test project setup. -async function getDataSources(id: string) { - await sdk.makeAuthenticatedRequest( - `https://developer-applications.ciscospark.com/v1/dataSources/${id}` - ); -} - -export {listDataSources, getDataSources}; - -export default listDataSources; diff --git a/packages/byods/src/base-client/index.ts b/packages/byods/src/base-client/index.ts new file mode 100644 index 00000000000..12b10dca82f --- /dev/null +++ b/packages/byods/src/base-client/index.ts @@ -0,0 +1,201 @@ +import fetch, {Response, RequestInit} from 'node-fetch'; + +import TokenManager from '../token-manager'; +import DataSourceClient from '../data-source-client'; +import {HttpClient, ApiResponse} from '../http-client/types'; + +export default class BaseClient { + private baseUrl: string; + private headers: Record; + private tokenManager: TokenManager; + private orgId: string; + + public dataSource: DataSourceClient; + + /** + * Creates an instance of BaseClient. + * @param {string} baseUrl - The base URL for the API. + * @param {Record} headers - The additional headers to be used in requests. + * @param {TokenManager} tokenManager - The token manager instance. + * @param {string} orgId - The organization ID. + * @example + * const client = new BaseClient('https://webexapis.com/v1', { 'Your-Custom-Header': 'some value' }, tokenManager, 'org123'); + */ + constructor( + baseUrl: string, + headers: Record, + tokenManager: TokenManager, + orgId: string + ) { + this.baseUrl = baseUrl; + this.headers = headers; + this.tokenManager = tokenManager; + this.orgId = orgId; + this.dataSource = new DataSourceClient(this.getHttpClientForOrg()); + } + + /** + * Makes an HTTP request. + * @param {string} endpoint - The API endpoint. + * @param {RequestInit} [options=\{\}] - The request options. + * @returns {Promise>} - The API response. + * @template T + * @example + * const response = await client.request('/endpoint', { method: 'GET', headers: {} }); + */ + public async request(endpoint: string, options: RequestInit = {}): Promise> { + const url = `${this.baseUrl}${endpoint}`; + const token = await this.getToken(); + + const response: Response = await fetch(url, { + ...options, + headers: { + Authorization: `Bearer ${token}`, + ...this.headers, + ...options.headers, + }, + }); + + const data: any = await response.json(); + if (!response.ok) { + throw new Error(`Error: ${response.status} - ${data.message}`); + } + + return {data, status: response.status}; + } + + /** + * Makes a POST request. + * @param {string} endpoint - The API endpoint. + * @param {Record} body - The request body. + * @param {Record} [headers=\{\}] - The request headers. + * @returns {Promise>} - The API response. + * @template T + * @example + * const response = await client.post('/endpoint', { key: 'value' }); + */ + public async post( + endpoint: string, + body: Record, + headers: Record = {} + ): Promise> { + return this.request(endpoint, { + method: 'POST', + body: JSON.stringify(body), + headers: {'Content-Type': 'application/json', ...headers}, + }); + } + + /** + * Makes a PUT request. + * @param {string} endpoint - The API endpoint. + * @param {Record} body - The request body. + * @returns {Promise>} - The API response. + * @template T + * @example + * const response = await client.put('/endpoint', { key: 'value' }); + */ + public async put( + endpoint: string, + body: Record, + headers: Record = {} + ): Promise> { + return this.request(endpoint, { + method: 'PUT', + body: JSON.stringify(body), + headers: {'Content-Type': 'application/json', ...headers}, + }); + } + + /** + * Makes a PATCH request. + * @param {string} endpoint - The API endpoint. + * @param {Record} body - The request body. + * @returns {Promise>} - The API response. + * @template T + * @example + * const response = await client.patch('/endpoint', { key: 'value' }); + */ + public async patch( + endpoint: string, + body: Record, + headers: Record = {} + ): Promise> { + return this.request(endpoint, { + method: 'PATCH', + body: JSON.stringify(body), + headers: {'Content-Type': 'application/json', ...headers}, + }); + } + + /** + * Makes a GET request. + * @param {string} endpoint - The API endpoint. + * @returns {Promise>} - The API response. + * @template T + * @example + * const response = await client.get('/endpoint'); + */ + public async get( + endpoint: string, + headers: Record = {} + ): Promise> { + return this.request(endpoint, { + method: 'GET', + headers, + }); + } + + /** + * Makes a DELETE request. + * @param {string} endpoint - The API endpoint. + * @returns {Promise>} - The API response. + * @template T + * @example + * const response = await client.delete('/endpoint'); + */ + public async delete( + endpoint: string, + headers: Record = {} + ): Promise> { + return this.request(endpoint, { + method: 'DELETE', + headers, + }); + } + + /** + * Get an HTTP client for a specific organization. + * @returns {HttpClient} - An object containing methods for making HTTP requests. + * @example + * const httpClient = client.getHttpClientForOrg(); + * const response = await httpClient.get('/endpoint'); + */ + public getHttpClientForOrg(): HttpClient { + return { + get: (endpoint: string) => this.get(endpoint), + delete: (endpoint: string) => this.delete(endpoint), + post: (endpoint: string, body: Record) => this.post(endpoint, body), + put: (endpoint: string, body: Record) => this.put(endpoint, body), + patch: (endpoint: string, body: Record) => this.patch(endpoint, body), + }; + } + + private async getToken(): Promise { + const serviceAppAuthorization = await this.tokenManager.getOrgServiceAppAuthorization( + this.orgId + ); + let token = serviceAppAuthorization.serviceAppToken.accessToken; + + if (new Date() >= new Date(serviceAppAuthorization.serviceAppToken.expiresAt)) { + await this.tokenManager.refreshServiceAppAccessToken(this.orgId, this.headers); + const refreshedAuthorization = await this.tokenManager.getOrgServiceAppAuthorization( + this.orgId + ); + token = refreshedAuthorization.serviceAppToken.accessToken; + } + // TODO: Handle refresh token expiration + + return token; + } +} diff --git a/packages/byods/src/byods/index.ts b/packages/byods/src/byods/index.ts new file mode 100644 index 00000000000..7f2996ad557 --- /dev/null +++ b/packages/byods/src/byods/index.ts @@ -0,0 +1,91 @@ +import {jwksCache, createRemoteJWKSet, JWKSCacheInput} from 'jose'; + +import BaseClient from '../base-client'; +import { + USER_AGENT, + PRODUCTION_JWKS_URL, + INTEGRATION_JWKS_URL, + PRODUCTION_BASE_URL, + INTEGRATION_BASE_URL, +} from '../constants'; +import {SDKConfig} from '../types'; +import TokenManager from '../token-manager'; + +/** + * The BYoDS SDK. + */ +export default class BYODS { + private headers: Record = { + 'User-Agent': USER_AGENT, + }; + + private jwksCache: JWKSCacheInput = {}; + private jwks: any; // No defined interface for return type of createRemoteJWKSet + private env: 'production' | 'integration'; + private config: SDKConfig; + private baseUrl: string; + + /** + * The token manager for the SDK. + */ + public tokenManager: TokenManager; + + /** + * Constructs a new instance of the BYODS SDK. + * + * @param {SDKConfig} config - The configuration object containing clientId and clientSecret. + * @example + * const sdk = new BYODS({ clientId: 'your-client-id', clientSecret: 'your-client-secret' }); + */ + constructor({clientId, clientSecret}: SDKConfig) { + this.config = {clientId, clientSecret}; + this.tokenManager = new TokenManager(clientId, clientSecret); + + /** + * The environment variable `process.env.BYODS_ENVIRONMENT` determines the environment in which the SDK operates. + * It can be set to either 'production' or 'integration'. If not set, it defaults to 'production'. + */ + const parsedEnv = process.env.BYODS_ENVIRONMENT || 'production'; + let jwksUrl = PRODUCTION_BASE_URL; + + switch (parsedEnv) { + case 'production': + this.env = 'production'; + this.baseUrl = PRODUCTION_BASE_URL; + jwksUrl = PRODUCTION_JWKS_URL; + break; + case 'integration': + this.env = 'integration'; + this.baseUrl = INTEGRATION_BASE_URL; + jwksUrl = INTEGRATION_JWKS_URL; + break; + default: + this.env = 'production'; + this.baseUrl = PRODUCTION_BASE_URL; + jwksUrl = PRODUCTION_JWKS_URL; + } + + // Create a remote JWK Set + this.jwks = createRemoteJWKSet(new URL(jwksUrl), { + [jwksCache]: this.jwksCache, + cacheMaxAge: 600000, // 10 minutes + cooldownDuration: 30000, // 30 seconds + }); + } + + /** + * Retrieves a client instance for a specific organization. + * + * @param {string} orgId - The unique identifier of the organization. + * @returns {BaseClient} A new instance of BaseClient configured for the specified organization. + * @example + * const client = sdk.getClientForOrg('org-id'); + */ + public getClientForOrg(orgId: string): BaseClient { + if (!orgId) { + throw new Error(`orgId is required`); + } + + return new BaseClient(this.baseUrl, this.headers, this.tokenManager, orgId); + } +} diff --git a/packages/byods/src/constants.ts b/packages/byods/src/constants.ts new file mode 100644 index 00000000000..8b76cab1ea5 --- /dev/null +++ b/packages/byods/src/constants.ts @@ -0,0 +1,10 @@ +export const BYODS_FILE = 'BYODS'; +export const BYODS_SDK_VERSION = '0.0.1'; +export const BYODS_PACKAGE_NAME = 'BYoDS NodeJS SDK'; +export const USER_AGENT = `${BYODS_PACKAGE_NAME}/${BYODS_SDK_VERSION}`; +export const PRODUCTION_BASE_URL = 'https://webexapis.com/v1'; +export const INTEGRATION_BASE_URL = 'https://integration.webexapis.com/v1'; +export const PRODUCTION_JWKS_URL = 'https://idbroker.webex.com/idb/oauth2/v2/keys/verificationjwk'; +export const INTEGRATION_JWKS_URL = + 'https://idbrokerbts.webex.com/idb/oauth2/v2/keys/verificationjwk'; +export const APPLICATION_ID_PREFIX = 'ciscospark://us/APPLICATION/'; diff --git a/packages/byods/src/data-source-client/constants.ts b/packages/byods/src/data-source-client/constants.ts new file mode 100644 index 00000000000..2a8462b19c4 --- /dev/null +++ b/packages/byods/src/data-source-client/constants.ts @@ -0,0 +1 @@ +export const DATASOURCE_ENDPOINT = '/dataSources'; diff --git a/packages/byods/src/data-source-client/index.ts b/packages/byods/src/data-source-client/index.ts new file mode 100644 index 00000000000..c1b5d7abd62 --- /dev/null +++ b/packages/byods/src/data-source-client/index.ts @@ -0,0 +1,89 @@ +import {DataSourceRequest, DataSourceResponse} from './types'; +import {DATASOURCE_ENDPOINT} from './constants'; +import {HttpClient, ApiResponse} from '../http-client/types'; + +/** + * Client for interacting with the /dataSource API. + */ +export default class DataSourceClient { + private httpClient: HttpClient; + + /** + * Creates an instance of DataSourceClient. + * @param {HttpClient} httpClient - The HttpClient instance to use for API requests. + * @example + * const httpClient = new HttpClient(); + * const client = new DataSourceClient(httpClient); + */ + constructor(httpClient: HttpClient) { + this.httpClient = httpClient; + } + + /** + * Creates a new data source. + * @param {DataSourceRequest} createDataSourceRequest - The request object for creating a data source. + * @returns {Promise>} - A promise that resolves to the API response containing the created data source. + * @example + * const request: DataSourceRequest = { name: 'New DataSource', url: 'https://mydatasource.com', schemaId: '123', audience: 'myaudience', subject: 'mysubject', nonce: 'uniqueNonce' }; + * const response = await client.create(request); + */ + public async create( + dataSourcePayload: DataSourceRequest + ): Promise> { + return this.httpClient.post(DATASOURCE_ENDPOINT, dataSourcePayload); // TODO: Move /dataSources to constants + } + + /** + * Retrieves a data source by ID. + * @param {string} id - The ID of the data source to retrieve. + * @returns {Promise>} - A promise that resolves to the API response containing the data source. + * @example + * const id = '123'; + * const response = await client.get(id); + */ + public async get(id: string): Promise> { + return this.httpClient.get(`${DATASOURCE_ENDPOINT}/${id}`); + } + + /** + * Lists all data sources. + * @returns {Promise>} - A promise that resolves to the API response containing the list of data sources. + * @example + * const response = await client.list(); + */ + public async list(): Promise> { + return this.httpClient.get(DATASOURCE_ENDPOINT); + } + + /** + * Updates a data source by ID. + * @param {string} id - The ID of the data source to update. + * @param {DataSourceRequest} updateDataSourceRequest - The request object for updating a data source. + * @returns {Promise>} - A promise that resolves to the API response containing the updated data source. + * @example + * const id = '123'; + * const request: DataSourceRequest = { name: 'Updated DataSource', url: 'https://mydatasource.com', schemaId: '123', audience: 'myaudience', subject: 'mysubject', nonce: 'uniqueNonce' }; + * const response = await client.update(id, request); + */ + public async update( + id: string, + dataSourcePayload: DataSourceRequest + ): Promise> { + return this.httpClient.put( + `${DATASOURCE_ENDPOINT}/${id}`, + dataSourcePayload + ); + } + + /** + * Deletes a data source by ID. + * @param {string} id - The ID of the data source to delete. + * @returns {Promise>} - A promise that resolves to the API response confirming the deletion. + * @example + * const id = '123'; + * const response = await client.delete(id); + */ + public async delete(id: string): Promise> { + return this.httpClient.delete(`${DATASOURCE_ENDPOINT}/${id}`); + } +} diff --git a/packages/byods/src/data-source-client/types.ts b/packages/byods/src/data-source-client/types.ts new file mode 100644 index 00000000000..923b0af6c2b --- /dev/null +++ b/packages/byods/src/data-source-client/types.ts @@ -0,0 +1,98 @@ +/** + * Represents the response from a data source. + * + * @public + */ +export interface DataSourceResponse { + /** + * The unique identifier for the data source response. + */ + id: string; + + /** + * The identifier for the schema associated with the data source. + */ + schemaId: string; + + /** + * The identifier for the organization associated with the data source. + */ + orgId: string; + + /** + * The plain client identifier (not a Hydra base64 id string). + */ + applicationId: string; + + /** + * The status of the data source response. Either "active" or "disabled". + */ + status: string; + + /** + * The JSON Web Signature token associated with the data source response. + */ + jwsToken: string; + + /** + * The identifier of the user who created the data source response. + */ + createdBy: string; + + /** + * The timestamp when the data source response was created. + */ + createdAt: string; + + /** + * The identifier of the user who last updated the data source response. + */ + updatedBy?: string; + + /** + * The timestamp when the data source response was last updated. + */ + updatedAt?: string; + + /** + * The error message associated with the data source response, if any. + */ + errorMessage?: string; +} + +/** + * Represents the request to a data source. + * + * @public + */ +export interface DataSourceRequest { + /** + * The identifier for the schema associated with the data source. + */ + schemaId: string; + + /** + * The URL of the data source. + */ + url: string; + + /** + * The audience for the data source request. + */ + audience: string; + + /** + * The subject of the data source request. + */ + subject: string; + + /** + * A unique nonce for the data source request. + */ + nonce: string; + + /** + * The lifetime of the token in minutes. + */ + tokenLifetimeMinutes: number; +} diff --git a/packages/byods/src/http-client/types.ts b/packages/byods/src/http-client/types.ts new file mode 100644 index 00000000000..f882efa77d8 --- /dev/null +++ b/packages/byods/src/http-client/types.ts @@ -0,0 +1,59 @@ +/** + * Represents a generic API response. + * + * @public + */ +export interface ApiResponse { + /** + * The response data. + */ + data: T; + + /** + * The response status code. + */ + status: number; +} + +/** + * Interface representing an HTTP client. + */ +export interface HttpClient { + /** + * Make a GET request to the specified endpoint. + * @param {string} endpoint - The endpoint to send the GET request to. + * @returns {Promise>} - A promise that resolves to the response data. + */ + get(endpoint: string): Promise>; + + /** + * Make a DELETE request to the specified endpoint. + * @param {string} endpoint - The endpoint to send the DELETE request to. + * @returns {Promise>} - A promise that resolves to the response data. + */ + delete(endpoint: string): Promise>; + + /** + * Make a POST request to the specified endpoint with the given body. + * @param {string} endpoint - The endpoint to send the POST request to. + * @param {Record} body - The body of the POST request. + * @returns {Promise>} - A promise that resolves to the response data. + */ + post(endpoint: string, body: Record): Promise>; + + /** + * Make a PUT request to the specified endpoint with the given body. + * @param {string} endpoint - The endpoint to send the PUT request to. + * @param {Record} body - The body of the PUT request. + * @returns {Promise>} - A promise that resolves to the response data. + */ + put(endpoint: string, body: Record): Promise>; + + /** + * Make a PATCH request to the specified endpoint with the given body. + * @param {string} endpoint - The endpoint to send the PATCH request to. + * @param {Record} body - The body of the PATCH request. + * @returns {Promise>} - A promise that resolves to the response data. + */ + patch(endpoint: string, body: Record): Promise>; +} diff --git a/packages/byods/src/index.ts b/packages/byods/src/index.ts index 158408b0be9..86847a6b329 100644 --- a/packages/byods/src/index.ts +++ b/packages/byods/src/index.ts @@ -1 +1,6 @@ -export {listDataSources, getDataSources} from './apiClient'; +import BYODS from './byods'; +import TokenManager from './token-manager'; +import BaseClient from './base-client'; +import DataSourceClient from './data-source-client'; + +export {BYODS, TokenManager, BaseClient, DataSourceClient}; diff --git a/packages/byods/src/token-manager/index.ts b/packages/byods/src/token-manager/index.ts new file mode 100644 index 00000000000..c88de4ecc7c --- /dev/null +++ b/packages/byods/src/token-manager/index.ts @@ -0,0 +1,186 @@ +import fetch, {Response} from 'node-fetch'; + +import {APPLICATION_ID_PREFIX, PRODUCTION_BASE_URL} from '../constants'; +import {TokenResponse, OrgServiceAppAuthorization, ServiceAppAuthorizationMap} from '../types'; + +/** + * The token manager for the BYoDS SDK. + */ +export default class TokenManager { + private serviceAppAuthorizations: ServiceAppAuthorizationMap = {}; + private clientId: string; + private clientSecret: string; + private serviceAppId: string; + private baseUrl: string; + + /** + * Creates an instance of TokenManager. + * + * @param clientId - The client ID of the service app. + * @param clientSecret - The client secret of the service app. + * @param baseUrl - The base URL for the API. Defaults to `PRODUCTION_BASE_URL`. + * @example + * const tokenManager = new TokenManager('your-client-id', 'your-client-secret'); + */ + constructor(clientId: string, clientSecret: string, baseUrl: string = PRODUCTION_BASE_URL) { + if (!clientId || !clientSecret) { + throw new Error('clientId and clientSecret are required'); + } + this.clientId = clientId; + this.clientSecret = clientSecret; + this.baseUrl = baseUrl; + this.serviceAppId = Buffer.from(`${APPLICATION_ID_PREFIX}${clientId}`).toString('base64'); + } + + /** + * Update the tokens and their expiration times. + * @param {TokenResponse} data - The token response data. + * @param {string} orgId - The organization ID. + * @returns {void} + * @example + * tokenManager.updateServiceAppToken(tokenResponse, 'org-id'); + */ + public updateServiceAppToken(data: TokenResponse, orgId: string): void { + this.serviceAppAuthorizations[orgId] = { + orgId, + serviceAppToken: { + accessToken: data.access_token, + refreshToken: data.refresh_token, + expiresAt: new Date(Date.now() + data.expires_in * 1000), + refreshAccessTokenExpiresAt: new Date(Date.now() + data.refresh_token_expires_in * 1000), + }, + }; + } + + /** + * Get the service app ID. + * @returns {string} + * @example + * const serviceAppId = tokenManager.getServiceAppId(); + */ + public getServiceAppId(): string { + return this.serviceAppId; + } + + /** + * Get the service app authorization data for a given organization ID. + * @param {string} orgId - The organization ID. + * @returns {Promise} + * @example + * const authorization = await tokenManager.getOrgServiceAppAuthorization('org-id'); + */ + public async getOrgServiceAppAuthorization(orgId: string): Promise { + if (!this.serviceAppAuthorizations[orgId]) { + return Promise.reject(new Error('Service app authorization not found')); + } + + return Promise.resolve(this.serviceAppAuthorizations[orgId]); + } + + /** + * Retrieve a new service app token using the service app owner's personal access token(PAT). + * @param {string} orgId - The organization ID. + * @param {string} personalAccessToken - The service app owner's personal access token or token from an integration that has the scope `spark:applications_token`. + * @returns {Promise} + * await tokenManager.getServiceAppTokenUsingPAT('org-id', 'personal-access-token'); + */ + public async getServiceAppTokenUsingPAT( + orgId: string, + personalAccessToken: string, + headers: Record = {} + ): Promise { + try { + const response: Response = await fetch( + `${this.baseUrl}/applications/${this.serviceAppId}/token`, + { + method: 'POST', + headers: { + Authorization: `Bearer ${personalAccessToken}`, + 'Content-Type': 'application/json', + ...headers, + }, + body: JSON.stringify({ + targetOrgId: orgId, + clientId: this.clientId, + clientSecret: this.clientSecret, + }), + } + ); + + if (!response.ok) { + throw new Error(`Failed to retrieve token: ${response.statusText}`); + } + + const data: TokenResponse = (await response.json()) as TokenResponse; + this.updateServiceAppToken(data, orgId); + } catch (error) { + console.error('Error retrieving token after authorization:', error); + throw error; + } + } + + /** + * Refresh the access token using the refresh token. + * @param {string} orgId - The organization ID. + * @returns {Promise} + * await tokenManager.refreshServiceAppAccessToken('org-id'); + */ + public async refreshServiceAppAccessToken( + orgId: string, + headers: Record = {} + ): Promise { + if (!orgId) { + throw new Error('orgId not provided'); + } + + const serviceAppAuthorization = await this.getOrgServiceAppAuthorization(orgId); + const refreshToken = serviceAppAuthorization?.serviceAppToken.refreshToken; + + if (!refreshToken) { + throw new Error(`Refresh token was not found for org:${orgId}`); + } + + await this.saveServiceAppRegistrationData(orgId, refreshToken, headers); + } + + /** + * Save the service app registration using the provided refresh token. + * After saving, it can be fetched by using the {@link getOrgServiceAppAuthorization} method. + * @param {string} orgId - The organization ID. + * @param {string} refreshToken - The refresh token. + * @returns {Promise} + * @example + * await tokenManager.saveServiceAppRegistrationData('org-id', 'refresh-token'); + */ + public async saveServiceAppRegistrationData( + orgId: string, + refreshToken: string, + headers: Record = {} + ): Promise { + try { + const response: Response = await fetch(`${this.baseUrl}/access_token`, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', // https://developer.webex.com/docs/login-with-webex#access-token-endpoint + ...headers, + }, + body: new URLSearchParams({ + grant_type: 'refresh_token', + client_id: this.clientId, + client_secret: this.clientSecret, + refresh_token: refreshToken, + }), + }); + + if (!response.ok) { + throw new Error(`Failed to save service app registration: ${response.statusText}`); + } + + const data: TokenResponse = (await response.json()) as TokenResponse; + this.updateServiceAppToken(data, orgId); + } catch (error) { + console.error('Error saving service app registration:', error); + throw error; + } + } +} diff --git a/packages/byods/src/types.ts b/packages/byods/src/types.ts new file mode 100644 index 00000000000..d527727a931 --- /dev/null +++ b/packages/byods/src/types.ts @@ -0,0 +1,104 @@ +/** + * Configuration options for the SDK. + * + * @public + */ +export interface SDKConfig { + /** + * The client ID of the service app. + */ + clientId: string; + + /** + * The client secret of the service app. + */ + clientSecret: string; +} + +/** + * TokenResponse JSON shape from Webex APIs. + * + * @public + */ +export interface TokenResponse { + /** + * The access token. + */ + access_token: string; + + /** + * The expiration time of the access token in seconds. + */ + expires_in: number; + + /** + * The refresh token. + */ + refresh_token: string; + + /** + * The expiration time of the refresh token in seconds. + */ + refresh_token_expires_in: number; + + /** + * The type of the token. + */ + token_type: string; +} + +/** + * Represents a token with its expiration details. + * + * @public + */ +export interface ServiceAppToken { + /** + * The access token. + */ + accessToken: string; + + /** + * The refresh token. + */ + refreshToken: string; + + /** + * The expiration date of the access token. + */ + expiresAt: Date; + + /** + * The expiration date of the refresh token. + */ + refreshAccessTokenExpiresAt: Date; +} + +/** + * Represents a service app authorization token info for an organization. + * + * @public + */ +export interface OrgServiceAppAuthorization { + /** + * The organization ID. + */ + orgId: string; + + /** + * The token details. + */ + serviceAppToken: ServiceAppToken; +} + +/** + * Represents a map of service app authorizations to the orgId. + * + * @public + */ +export interface ServiceAppAuthorizationMap { + /** + * The organization ID mapped to its authorization details. + */ + [orgId: string]: OrgServiceAppAuthorization; +} diff --git a/packages/byods/test/unit/spec/base-client/index.ts b/packages/byods/test/unit/spec/base-client/index.ts new file mode 100644 index 00000000000..995cbe18e23 --- /dev/null +++ b/packages/byods/test/unit/spec/base-client/index.ts @@ -0,0 +1,70 @@ +import BaseClient from '../../../../src/base-client'; +import TokenManager from '../../../../src/token-manager'; +import DataSourceClient from '../../../../src/data-source-client'; +import {PRODUCTION_BASE_URL} from '../../../../src/constants'; + +describe('BaseClient Tests', () => { + const baseClient: BaseClient = new BaseClient( + PRODUCTION_BASE_URL, + {}, + new TokenManager('clientId', 'clientSecret'), + 'orgId' + ); + + it('creates an instance of BaseClient', () => { + expect(baseClient).toBeInstanceOf(BaseClient); + }); + + it('should make a GET request', async () => { + const mockResponse = {data: 'test', status: 200}; + jest.spyOn(baseClient, 'request').mockResolvedValue(mockResponse); + + const response = await baseClient.get('/test-endpoint'); + expect(response).toEqual(mockResponse); + }); + + it('should make a POST request', async () => { + const mockResponse = {data: 'test', status: 200}; + jest.spyOn(baseClient, 'request').mockResolvedValue(mockResponse); + + const response = await baseClient.post('/test-endpoint', {key: 'value'}); + expect(response).toEqual(mockResponse); + }); + + it('should make a PUT request', async () => { + const mockResponse = {data: 'test', status: 200}; + jest.spyOn(baseClient, 'request').mockResolvedValue(mockResponse); + + const response = await baseClient.put('/test-endpoint', {key: 'value'}); + expect(response).toEqual(mockResponse); + }); + + it('should make a PATCH request', async () => { + const mockResponse = {data: 'test', status: 200}; + jest.spyOn(baseClient, 'request').mockResolvedValue(mockResponse); + + const response = await baseClient.patch('/test-endpoint', {key: 'value'}); + expect(response).toEqual(mockResponse); + }); + + it('should make a DELETE request', async () => { + const mockResponse = {data: 'test', status: 200}; + jest.spyOn(baseClient, 'request').mockResolvedValue(mockResponse); + + const response = await baseClient.delete('/test-endpoint'); + expect(response).toEqual(mockResponse); + }); + + it('should get an HTTP client for org', () => { + const httpClient = baseClient.getHttpClientForOrg(); + expect(httpClient).toHaveProperty('get'); + expect(httpClient).toHaveProperty('post'); + expect(httpClient).toHaveProperty('put'); + expect(httpClient).toHaveProperty('patch'); + expect(httpClient).toHaveProperty('delete'); + }); + + it('should get a data source client', () => { + expect(baseClient.dataSource).toBeInstanceOf(DataSourceClient); + }); +}); diff --git a/packages/byods/test/unit/spec/byods/index.ts b/packages/byods/test/unit/spec/byods/index.ts new file mode 100644 index 00000000000..525e8d82c83 --- /dev/null +++ b/packages/byods/test/unit/spec/byods/index.ts @@ -0,0 +1,32 @@ +import BYODS from '../../../../src/byods'; +import TokenManager from '../../../../src/token-manager'; +import BaseClient from '../../../../src/base-client'; +import {SDKConfig} from '../../../../src/types'; +import DataSourceClient from '../../../../src/data-source-client'; + +jest.mock('node-fetch', () => jest.fn()); + +describe('BYODS Tests', () => { + const mockSDKConfig: SDKConfig = { + clientId: 'your-client-id', + clientSecret: 'your-client-secret', + }; + + const sdk = new BYODS(mockSDKConfig); + + it('should create an instance of BYODS', () => { + expect(sdk).toBeInstanceOf(BYODS); + }); + + it('should initialize TokenManager with correct parameters', () => { + expect(sdk.tokenManager).toBeInstanceOf(TokenManager); + }); + + it('should get a client for an organization', () => { + expect(sdk.getClientForOrg('myOrgId')).toBeInstanceOf(BaseClient); + }); + + it('should configure DataSourceClient with correct parameters', () => { + expect(sdk.getClientForOrg('myOrgId').dataSource).toBeInstanceOf(DataSourceClient); + }); +}); diff --git a/packages/byods/test/unit/spec/data-source-client/index.ts b/packages/byods/test/unit/spec/data-source-client/index.ts new file mode 100644 index 00000000000..5069c2f417a --- /dev/null +++ b/packages/byods/test/unit/spec/data-source-client/index.ts @@ -0,0 +1,204 @@ +import DataSourceClient from '../../../../src/data-source-client'; +import {DataSourceRequest, DataSourceResponse} from '../../../../src/data-source-client/types'; +import {HttpClient, ApiResponse} from '../../../../src/http-client/types'; + +describe('DataSourceClient', () => { + let httpClient: jest.Mocked; + let dataSourceClient: DataSourceClient; + + beforeEach(() => { + httpClient = { + post: jest.fn(), + get: jest.fn(), + put: jest.fn(), + patch: jest.fn(), + delete: jest.fn(), + }; + dataSourceClient = new DataSourceClient(httpClient); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should create a new data source', async () => { + const request: DataSourceRequest = { + schemaId: 'myschemaid', + url: 'https://mydatasource.com', + audience: 'myaudience', + subject: 'mysubject', + nonce: 'uniqueNonce', + tokenLifetimeMinutes: 60, + }; + const response: ApiResponse = { + status: 201, + data: { + id: '123', + schemaId: 'myschemaid', + orgId: 'org123', + applicationId: 'app123', + status: 'active', + jwsToken: 'someJwsToken', + createdBy: 'someUser', + createdAt: '2024-01-01T00:00:00Z', + }, + }; + httpClient.post.mockResolvedValue(response); + + const result = await dataSourceClient.create(request); + + expect(httpClient.post).toHaveBeenCalledWith('/dataSources', request); + expect(result).toEqual(response); + }); + + it('should retrieve a data source by ID', async () => { + const id = '123'; + const response: ApiResponse = { + status: 200, + data: { + id: '123', + schemaId: 'myschemaid', + orgId: 'org123', + applicationId: 'app123', + status: 'active', + jwsToken: 'someJwsToken', + createdBy: 'someUser', + createdAt: '2024-01-01T00:00:00Z', + }, + }; + httpClient.get.mockResolvedValue(response); + + const result = await dataSourceClient.get(id); + + expect(httpClient.get).toHaveBeenCalledWith(`/dataSources/${id}`); + expect(result).toEqual(response); + }); + + it('should list all data sources', async () => { + const response: ApiResponse = { + data: [ + { + id: '123', + schemaId: 'myschemaid', + orgId: 'org123', + applicationId: 'app123', + status: 'active', + jwsToken: 'someJwsToken', + createdBy: 'someUser', + createdAt: '2024-01-01T00:00:00Z', + }, + ], + status: 200, + }; + httpClient.get.mockResolvedValue(response); + + const result = await dataSourceClient.list(); + + expect(httpClient.get).toHaveBeenCalledWith('/dataSources'); + expect(result).toEqual(response); + }); + + it('should update a data source by ID', async () => { + const id = '123'; + const request: DataSourceRequest = { + schemaId: 'updatedSchemaId', + url: 'https://updateddatasource.com', + audience: 'updatedAudience', + subject: 'updatedSubject', + nonce: 'updatedNonce', + tokenLifetimeMinutes: 60, + }; + const response: ApiResponse = { + status: 200, + data: { + id: '123', + schemaId: 'updatedSchemaId', + orgId: 'org123', + applicationId: 'app123', + status: 'active', + jwsToken: 'updatedJwsToken', + createdBy: 'someUser', + createdAt: '2024-01-01T00:00:00Z', + }, + }; + httpClient.put.mockResolvedValue(response); + + const result = await dataSourceClient.update(id, request); + + expect(httpClient.put).toHaveBeenCalledWith(`/dataSources/${id}`, request); + expect(result).toEqual(response); + }); + + it('should delete a data source by ID', async () => { + const id = '123'; + const response: ApiResponse = { + data: undefined, + status: 204, + }; + httpClient.delete.mockResolvedValue(response); + + const result = await dataSourceClient.delete(id); + + expect(httpClient.delete).toHaveBeenCalledWith(`/dataSources/${id}`); + expect(result).toEqual(response); + }); + + it('should handle errors when creating a data source', async () => { + const request: DataSourceRequest = { + schemaId: 'myschemaid', + url: 'https://mydatasource.com', + audience: 'myaudience', + subject: 'mysubject', + nonce: 'uniqueNonce', + tokenLifetimeMinutes: 60, + }; + const error = new Error('Network error'); + httpClient.post.mockRejectedValue(error); + + await expect(dataSourceClient.create(request)).rejects.toThrow('Network error'); + expect(httpClient.post).toHaveBeenCalledWith('/dataSources', request); + }); + + it('should handle errors when retrieving a data source by ID', async () => { + const id = '123'; + const error = new Error('Network error'); + httpClient.get.mockRejectedValue(error); + + await expect(dataSourceClient.get(id)).rejects.toThrow('Network error'); + expect(httpClient.get).toHaveBeenCalledWith(`/dataSources/${id}`); + }); + + it('should handle errors when listing all data sources', async () => { + const error = new Error('Network error'); + httpClient.get.mockRejectedValue(error); + + await expect(dataSourceClient.list()).rejects.toThrow('Network error'); + expect(httpClient.get).toHaveBeenCalledWith('/dataSources'); + }); + + it('should handle errors when updating a data source by ID', async () => { + const id = '123'; + const request: DataSourceRequest = { + schemaId: 'updatedSchemaId', + url: 'https://updateddatasource.com', + audience: 'updatedAudience', + subject: 'updatedSubject', + nonce: 'updatedNonce', + tokenLifetimeMinutes: 120, + }; + const error = new Error('Network error'); + httpClient.put.mockRejectedValue(error); + + await expect(dataSourceClient.update(id, request)).rejects.toThrow('Network error'); + expect(httpClient.put).toHaveBeenCalledWith(`/dataSources/${id}`, request); + }); + + it('should handle errors when deleting a data source by ID', async () => { + const id = '123'; + const error = new Error('Network error'); + httpClient.delete.mockRejectedValue(error); + + await expect(dataSourceClient.delete(id)).rejects.toThrowError('Network error'); + expect(httpClient.delete).toHaveBeenCalledWith(`/dataSources/${id}`); + }); +}); diff --git a/packages/byods/test/unit/spec/token-manager/index.ts b/packages/byods/test/unit/spec/token-manager/index.ts new file mode 100644 index 00000000000..0fd400a5b48 --- /dev/null +++ b/packages/byods/test/unit/spec/token-manager/index.ts @@ -0,0 +1,144 @@ +import fetch, {Response} from 'node-fetch'; + +import TokenManager from '../../../../src/token-manager'; +import {TokenResponse} from '../../../../src/types'; + +jest.mock('node-fetch', () => jest.fn()); + +describe('TokenManager', () => { + const clientId = 'test-client-id'; + const clientSecret = 'test-client-secret'; + const baseUrl = 'https://webexapis.com/v1'; + const orgId = 'test-org-id'; + const personalAccessToken = 'test-personal-access-token'; + const refreshToken = 'test-refresh-token'; + + let tokenManager: TokenManager; + + beforeEach(() => { + (fetch as jest.Mock).mockClear(); + tokenManager = new TokenManager(clientId, clientSecret, baseUrl); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it('should update service app token', async () => { + const tokenResponse: TokenResponse = { + access_token: 'new-access-token', + refresh_token: 'new-refresh-token', + expires_in: 3600, + token_type: 'Bearer', + refresh_token_expires_in: 7200, + }; + + tokenManager.updateServiceAppToken(tokenResponse, orgId); + + const serviceAppAuthorization = await tokenManager.getOrgServiceAppAuthorization(orgId); + expect(serviceAppAuthorization).toBeDefined(); + expect(serviceAppAuthorization.serviceAppToken.accessToken).toBe('new-access-token'); + expect(serviceAppAuthorization.serviceAppToken.refreshToken).toBe('new-refresh-token'); + }); + + it('should get service app authorization', async () => { + const tokenResponse: TokenResponse = { + access_token: 'new-access-token', + refresh_token: 'new-refresh-token', + expires_in: 3600, + token_type: 'Bearer', + refresh_token_expires_in: 7200, + }; + + tokenManager.updateServiceAppToken(tokenResponse, orgId); + + const serviceAppAuthorization = await tokenManager.getOrgServiceAppAuthorization(orgId); + expect(serviceAppAuthorization).toBeDefined(); + expect(serviceAppAuthorization.serviceAppToken.accessToken).toBe('new-access-token'); + }); + + it('should throw error if service app authorization not found', async () => { + await expect(tokenManager.getOrgServiceAppAuthorization(orgId)).rejects.toThrow( + 'Service app authorization not found' + ); + }); + + it('should refresh service app access token', async () => { + const tokenResponse: TokenResponse = { + access_token: 'new-access-token', + refresh_token: refreshToken, + expires_in: 3600, + token_type: 'Bearer', + refresh_token_expires_in: 7200, + }; + + tokenManager.updateServiceAppToken(tokenResponse, orgId); + + const mockResponse = { + json: jest.fn().mockResolvedValue(tokenResponse), + ok: true, + } as unknown as Response; + + (fetch as unknown as jest.MockedFunction).mockResolvedValue(mockResponse); + + await tokenManager.refreshServiceAppAccessToken(orgId); + + expect(fetch).toHaveBeenCalledWith(`${baseUrl}/access_token`, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams({ + grant_type: 'refresh_token', + client_id: clientId, + client_secret: clientSecret, + refresh_token: refreshToken, + }), + }); + const serviceAppAuthorization = await tokenManager.getOrgServiceAppAuthorization(orgId); + expect(serviceAppAuthorization.serviceAppToken.accessToken).toBe('new-access-token'); + }); + + it('should throw error if refresh token is undefined', async () => { + await expect(tokenManager.refreshServiceAppAccessToken(orgId)).rejects.toThrow( + 'Service app authorization not found' + ); + }); + + it('should retrieve token after authorization', async () => { + const tokenResponse: TokenResponse = { + access_token: 'new-access-token', + refresh_token: 'new-refresh-token', + token_type: 'Bearer', + expires_in: 3600, + refresh_token_expires_in: 7200, + }; + + const mockResponse = { + json: jest.fn().mockResolvedValue(tokenResponse), + ok: true, + } as unknown as Response; + + (fetch as unknown as jest.MockedFunction).mockResolvedValue(mockResponse); + + await tokenManager.getServiceAppTokenUsingPAT(orgId, personalAccessToken); + + expect(fetch).toHaveBeenCalledWith( + `${baseUrl}/applications/${tokenManager.getServiceAppId()}/token`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${personalAccessToken}`, + }, + body: JSON.stringify({ + targetOrgId: orgId, + clientId, + clientSecret, + }), + } + ); + const serviceAppAuthorization = await tokenManager.getOrgServiceAppAuthorization(orgId); + expect(serviceAppAuthorization.serviceAppToken.accessToken).toBe('new-access-token'); + }); +}); diff --git a/packages/byods/tsconfig.json b/packages/byods/tsconfig.json index 95a8783c6e1..7dbec17ff8a 100644 --- a/packages/byods/tsconfig.json +++ b/packages/byods/tsconfig.json @@ -106,7 +106,7 @@ }, "typedocOptions": { "entryPoints": [ - "./src/index.ts" + "./src/**" ], "sort": [ "source-order" @@ -120,7 +120,8 @@ }, "include": [ "src/**/*.ts", - "jest.global.d.ts" + "jest.global.d.ts", + "test/unit/spec/**/*.ts", ], "exclude": [ "./node_modules/**", diff --git a/yarn.lock b/yarn.lock index 58596af11ff..2527d5aca6e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5861,7 +5861,7 @@ __metadata: languageName: node linkType: hard -"@types/node-fetch@npm:^2.6.11": +"@types/node-fetch@npm:2.6.11": version: 2.6.11 resolution: "@types/node-fetch@npm:2.6.11" dependencies: @@ -7424,14 +7424,13 @@ __metadata: "@types/jest": 27.4.1 "@types/mocha": 9.0.0 "@types/node": 16.11.9 - "@types/node-fetch": ^2.6.11 + "@types/node-fetch": 2.6.11 "@types/uuid": 8.3.4 "@typescript-eslint/eslint-plugin": 5.38.1 "@typescript-eslint/parser": 5.38.1 "@web/dev-server": 0.4.5 "@webex/jest-config-legacy": "workspace:*" "@webex/legacy-tools": "workspace:*" - "@webex/media-helpers": "workspace:*" chai: 4.3.4 cspell: 5.19.2 esbuild: ^0.17.19 @@ -7445,6 +7444,7 @@ __metadata: eslint-plugin-tsdoc: 0.2.14 jest: 27.5.1 jest-junit: 13.0.0 + jose: 5.8.0 karma: 6.4.3 karma-chai: 0.1.0 karma-chrome-launcher: 3.1.0 @@ -7458,7 +7458,7 @@ __metadata: karma-typescript: 5.5.3 karma-typescript-es6-transform: 5.5.3 mocha: 10.6.0 - node-fetch: ^3.3.2 + node-fetch: 3.3.2 prettier: 2.5.1 puppeteer: 22.13.0 rimraf: 3.0.2 @@ -21690,6 +21690,13 @@ __metadata: languageName: node linkType: hard +"jose@npm:5.8.0": + version: 5.8.0 + resolution: "jose@npm:5.8.0" + checksum: bb9cd97ac6ccb8148a8e23d6a7f61e5756a3373a7d65dd783051d8af409c3534bdc2a2c30ecd1820988ea943aba5755b2a45b86955c5765d71691bb0ddd45d61 + languageName: node + linkType: hard + "js-logger@npm:^1.6.1": version: 1.6.1 resolution: "js-logger@npm:1.6.1" @@ -25195,6 +25202,17 @@ __metadata: languageName: node linkType: hard +"node-fetch@npm:3.3.2, node-fetch@npm:^3.3.2": + version: 3.3.2 + resolution: "node-fetch@npm:3.3.2" + dependencies: + data-uri-to-buffer: ^4.0.0 + fetch-blob: ^3.1.4 + formdata-polyfill: ^4.0.10 + checksum: 06a04095a2ddf05b0830a0d5302699704d59bda3102894ea64c7b9d4c865ecdff2d90fd042df7f5bc40337266961cb6183dcc808ea4f3000d024f422b462da92 + languageName: node + linkType: hard + "node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.12": version: 2.7.0 resolution: "node-fetch@npm:2.7.0" @@ -25209,17 +25227,6 @@ __metadata: languageName: node linkType: hard -"node-fetch@npm:^3.3.2": - version: 3.3.2 - resolution: "node-fetch@npm:3.3.2" - dependencies: - data-uri-to-buffer: ^4.0.0 - fetch-blob: ^3.1.4 - formdata-polyfill: ^4.0.10 - checksum: 06a04095a2ddf05b0830a0d5302699704d59bda3102894ea64c7b9d4c865ecdff2d90fd042df7f5bc40337266961cb6183dcc808ea4f3000d024f422b462da92 - languageName: node - linkType: hard - "node-forge@npm:^1, node-forge@npm:^1.2.1": version: 1.3.1 resolution: "node-forge@npm:1.3.1" From 4fa79e51354dd2ecb6e90495b95853570071ec3f Mon Sep 17 00:00:00 2001 From: RAM1232 Date: Fri, 27 Sep 2024 17:13:47 +0530 Subject: [PATCH 5/5] feat(byods-sdk):added logger package to base-client,token-manager,bydos and data-source-client --- .../src/Errors/catalog/BYODSDeviceError.ts | 70 ++++++ .../byods/src/Errors/catalog/ExtendedError.ts | 22 ++ packages/byods/src/Errors/index.ts | 1 + packages/byods/src/Errors/types.ts | 60 +++++ packages/byods/src/Logger/index.ts | 211 ++++++++++++++++++ packages/byods/src/Logger/types.ts | 33 +++ packages/byods/src/base-client/constant.ts | 1 + packages/byods/src/base-client/index.ts | 12 +- packages/byods/src/base-client/type.ts | 107 +++++++++ packages/byods/src/byods/constant.ts | 1 + packages/byods/src/byods/index.ts | 10 +- packages/byods/src/byods/type.ts | 107 +++++++++ .../byods/src/data-source-client/constants.ts | 1 + .../byods/src/data-source-client/index.ts | 10 +- .../byods/src/data-source-client/types.ts | 27 +++ packages/byods/src/token-manager/constant.ts | 1 + packages/byods/src/token-manager/index.ts | 27 ++- packages/byods/src/token-manager/type.ts | 107 +++++++++ .../byods/test/unit/spec/Logger/index.text.ts | 77 +++++++ 19 files changed, 877 insertions(+), 8 deletions(-) create mode 100644 packages/byods/src/Errors/catalog/BYODSDeviceError.ts create mode 100644 packages/byods/src/Errors/catalog/ExtendedError.ts create mode 100644 packages/byods/src/Errors/index.ts create mode 100644 packages/byods/src/Errors/types.ts create mode 100644 packages/byods/src/Logger/index.ts create mode 100644 packages/byods/src/Logger/types.ts create mode 100644 packages/byods/src/base-client/constant.ts create mode 100644 packages/byods/src/base-client/type.ts create mode 100644 packages/byods/src/byods/constant.ts create mode 100644 packages/byods/src/byods/type.ts create mode 100644 packages/byods/src/token-manager/constant.ts create mode 100644 packages/byods/src/token-manager/type.ts create mode 100644 packages/byods/test/unit/spec/Logger/index.text.ts diff --git a/packages/byods/src/Errors/catalog/BYODSDeviceError.ts b/packages/byods/src/Errors/catalog/BYODSDeviceError.ts new file mode 100644 index 00000000000..a46a0d85d84 --- /dev/null +++ b/packages/byods/src/Errors/catalog/BYODSDeviceError.ts @@ -0,0 +1,70 @@ +/* eslint-disable valid-jsdoc */ +//import {RegistrationStatus} from '../../common/types'; + +import {ErrorContext, ErrorMessage, ErrorObject, ERROR_TYPE} from '../types'; +import ExtendedError from './ExtendedError'; + +/** + * Any error reported from Calling client should be stored here. + */ +export class BYODSError extends ExtendedError { + // public status: RegistrationStatus = RegistrationStatus.INACTIVE; + + /** + * Instantiate the Error class with these parameters. + * + * @param {ErrorMessage} msg - Custom error message. + * @param {ErrorContext} context - The context in which the error occurred. + * @param {ERROR_TYPE} type - The type of the error. + * @param {RegistrationStatus} status - Mobius status, should be default. + */ + constructor( + msg: ErrorMessage, + // context: ErrorContext, + type: ERROR_TYPE, + // status: RegistrationStatus + ) { + super(msg,type); + // this.context = context; + // this.status = status; + } + + + + /** + * Class method exposed to callers to allow storing of error object. + * + * @param error - Error Object. + */ + public setError(error: ErrorObject) { + this.message = error.message; + // this.context = error.context; + this.type = error.type; + } + + /** + * Class method exposed to callers to retrieve error object. + * + * @returns Error. + */ + public getError(): ErrorObject { + return { message: this.message, type: this.type }; + } +} + +/** + * Instantiate CallingClientError. + * + * @param msg - Custom error message. + * @param context - Error context. + * @param type - Error Type. + * @param status - Mobius Status, should be default. + * @returns CallingClientError instance. + */ +export const createClientError = ( + msg: ErrorMessage, + context: ErrorContext, + type: ERROR_TYPE, + // status: RegistrationStatus + // ) => new CallingClientError(msg, context, type, status); +) => new BYODSError(msg,type); diff --git a/packages/byods/src/Errors/catalog/ExtendedError.ts b/packages/byods/src/Errors/catalog/ExtendedError.ts new file mode 100644 index 00000000000..0ce1527bb59 --- /dev/null +++ b/packages/byods/src/Errors/catalog/ExtendedError.ts @@ -0,0 +1,22 @@ +/* eslint-disable valid-jsdoc */ +import {ErrorContext, ErrorMessage, ERROR_TYPE} from '../types'; + +/** + * + */ +export default class ExtendedError extends Error { + public type: ERROR_TYPE; + + // public context: ErrorContext; + + /** + * @param msg - TODO. + * @param context - TODO. + * @param type - TODO. + */ + constructor(msg: ErrorMessage,type: ERROR_TYPE) { + super(msg); + this.type = type || ERROR_TYPE.DEFAULT; + // this.context = context; + } +} diff --git a/packages/byods/src/Errors/index.ts b/packages/byods/src/Errors/index.ts new file mode 100644 index 00000000000..563d98c1628 --- /dev/null +++ b/packages/byods/src/Errors/index.ts @@ -0,0 +1 @@ +export {BYODSError as BYODSError} from './catalog/BYODSDeviceError'; diff --git a/packages/byods/src/Errors/types.ts b/packages/byods/src/Errors/types.ts new file mode 100644 index 00000000000..ab33e4d7ba0 --- /dev/null +++ b/packages/byods/src/Errors/types.ts @@ -0,0 +1,60 @@ +import {IMetaContext} from '../Logger/types'; + +export type ErrorMessage = string; + +export enum ERROR_LAYER { + CALL_CONTROL = 'call_control', + MEDIA = 'media', +} + +export enum ERROR_TYPE { + CALL_ERROR = 'call_error', + DEFAULT = 'default_error', + FORBIDDEN_ERROR = 'forbidden', + NOT_FOUND = 'not_found', + REGISTRATION_ERROR = 'registration_error', + SERVICE_UNAVAILABLE = 'service_unavailable', + TIMEOUT = 'timeout', + TOKEN_ERROR = 'token_error', + SERVER_ERROR = 'server_error', +} + +export enum ERROR_CODE { + UNAUTHORIZED = 401, + FORBIDDEN = 403, + DEVICE_NOT_FOUND = 404, + INTERNAL_SERVER_ERROR = 500, + NOT_IMPLEMENTED = 501, + SERVICE_UNAVAILABLE = 503, + BAD_REQUEST = 400, + REQUEST_TIMEOUT = 408, + TOO_MANY_REQUESTS = 429, +} + +export enum CALL_ERROR_CODE { + INVALID_STATUS_UPDATE = 111, + DEVICE_NOT_REGISTERED = 112, + CALL_NOT_FOUND = 113, + ERROR_PROCESSING = 114, + USER_BUSY = 115, + PARSING_ERROR = 116, + TIMEOUT_ERROR = 117, + NOT_ACCEPTABLE = 118, + CALL_REJECTED = 119, + NOT_AVAILABLE = 120, +} + +export enum DEVICE_ERROR_CODE { + DEVICE_LIMIT_EXCEEDED = 101, + DEVICE_CREATION_DISABLED = 102, + DEVICE_CREATION_FAILED = 103, +} + +export interface ErrorContext extends IMetaContext {} + +export type ErrorObject = { + message: ErrorMessage; + type: ERROR_TYPE; + context: ErrorContext; +}; + diff --git a/packages/byods/src/Logger/index.ts b/packages/byods/src/Logger/index.ts new file mode 100644 index 00000000000..e2fde90bd25 --- /dev/null +++ b/packages/byods/src/Logger/index.ts @@ -0,0 +1,211 @@ +/* eslint-disable valid-jsdoc */ +import {BYODS_PACKAGE_NAME} from '../constants'; +import {IMetaContext} from './types'; +import ExtendedError from '../Errors/catalog/ExtendedError'; +import {LOGGING_LEVEL, LogContext, LOGGER, LOG_PREFIX} from './types'; + +/* + * These are the order of log levels :- + * error - 1 + * warn - 2 + * log - 3 + * info - 4 + * trace - 5 + * + * Where log level n denotes that level 1 -> level n will be logged. + */ + +let currentLogLevel = LOGGING_LEVEL.error; + +/** + * A wrapper around console which prints to stderr or stdout + * based on the level defined. + * + * @param message - Log Message to print. + * @param level - Log level. + */ +const writeToConsole = (message: string, level: LOGGER) => { + switch (level) { + case LOGGER.INFO: + case LOGGER.LOG: { + // eslint-disable-next-line no-console + console.log(message); + break; + } + case LOGGER.WARN: { + console.warn(message); + break; + } + case LOGGER.ERROR: { + console.error(message); + break; + } + case LOGGER.TRACE: { + // eslint-disable-next-line no-console + console.trace(message); + break; + } + default: { + // Since this is internal , we shouldn't reach here + } + } +}; + +/** + * Format the Log message as 'timestamp Calling SDK - [level]: file:example.ts - method:methodName - Actual log message'. + * + * @param context - File and method. + * @param level - Log level. + * @returns - Formatted string. + */ +const format = (context: IMetaContext, level: string): string => { + const timestamp = new Date().toUTCString(); + + return `${BYODS_PACKAGE_NAME}: ${timestamp}: ${level}: ${LOG_PREFIX.FILE}:${context.file} - ${LOG_PREFIX.METHOD}:${context.method}`; +}; + +/** + * Used by the Calling Client to initialize the logger module + * with a certain level. + * + * @param level - Log Level. + */ +const setLogger = (level: string, module: string) => { + switch (level) { + case LOGGER.WARN: { + currentLogLevel = LOGGING_LEVEL.warn; + break; + } + case LOGGER.LOG: { + currentLogLevel = LOGGING_LEVEL.log; + break; + } + case LOGGER.INFO: { + currentLogLevel = LOGGING_LEVEL.info; + break; + } + case LOGGER.TRACE: { + currentLogLevel = LOGGING_LEVEL.trace; + break; + } + default: { + currentLogLevel = LOGGING_LEVEL.error; + } + } + + const message = `Logger initialized for module: ${module} with level: ${currentLogLevel}`; + + writeToConsole( + `${format({file: 'logger.ts', method: 'setLogger'}, '')} - ${LOG_PREFIX.MESSAGE}:${message}`, + LOGGER.INFO + ); +}; + +/** + * To retrieve the current log level. + * + * @returns - Log level. + */ +const getLogLevel = (): LOGGER => { + let level; + + switch (currentLogLevel) { + case LOGGING_LEVEL.warn: { + level = LOGGER.WARN; + break; + } + case LOGGING_LEVEL.log: { + level = LOGGER.LOG; + break; + } + case LOGGING_LEVEL.info: { + level = LOGGER.INFO; + break; + } + case LOGGING_LEVEL.trace: { + level = LOGGER.TRACE; + break; + } + default: { + level = LOGGER.ERROR; + } + } + + return level; +}; + +/** + * Can be used to print only useful information. + * + * @param message - Caller emitted string. + * @param context - File and method which called. + */ +const logMessage = (message: string, context: LogContext) => { + if (currentLogLevel >= LOGGING_LEVEL.log) { + writeToConsole(`${format(context, '[LOG]')} - ${LOG_PREFIX.MESSAGE}:${message}`, LOGGER.LOG); + } +}; + +/** + * Can be used to print informational messages. + * + * @param message - Caller emitted string. + * @param context - File and method which called. + */ +const logInfo = (message: string, context: LogContext) => { + if (currentLogLevel >= LOGGING_LEVEL.info) { + writeToConsole(`${format(context, '[INFO]')} - ${LOG_PREFIX.MESSAGE}:${message}`, LOGGER.INFO); + } +}; + +/** + * Can be used to print warning messages. + * + * @param message - Caller emitted string. + * @param context - File and method which called. + */ +const logWarn = (message: string, context: LogContext) => { + if (currentLogLevel >= LOGGING_LEVEL.warn) { + writeToConsole(`${format(context, '[WARN]')} - ${LOG_PREFIX.MESSAGE}:${message}`, LOGGER.WARN); + } +}; + +/** + * Can be used to print the stack trace of the entire call path. + * + * @param message - Caller emitted string. + * @param context - File and method which called. + */ +const logTrace = (message: string, context: LogContext) => { + if (currentLogLevel >= LOGGING_LEVEL.trace) { + writeToConsole( + `${format(context, '[TRACE]')} - ${LOG_PREFIX.MESSAGE}:${message}`, + LOGGER.TRACE + ); + } +}; + +/** + * Can be used to print only errors. + * + * @param error - Error string . + * @param context - File and method which called. + */ +const logError = (error: ExtendedError, context: LogContext) => { + if (currentLogLevel >= LOGGING_LEVEL.error) { + writeToConsole( + `${format(context, '[ERROR]')} - !${LOG_PREFIX.ERROR}!${LOG_PREFIX.MESSAGE}:${error.message}`, + LOGGER.ERROR + ); + } +}; + +export default { + log: logMessage, + error: logError, + info: logInfo, + warn: logWarn, + trace: logTrace, + setLogger, + getLogLevel, +}; diff --git a/packages/byods/src/Logger/types.ts b/packages/byods/src/Logger/types.ts new file mode 100644 index 00000000000..5c0c5c9f8e4 --- /dev/null +++ b/packages/byods/src/Logger/types.ts @@ -0,0 +1,33 @@ +/* eslint-disable @typescript-eslint/no-empty-interface */ +// import {IMetaContext} from '../common/types'; +export interface IMetaContext { + file?: string; + method?: string; +} + +export interface LogContext extends IMetaContext {} + +export enum LOG_PREFIX { + MAIN = 'CALLING_SDK', + FILE = 'file', + METHOD = 'method', + EVENT = 'event', + MESSAGE = 'message', + ERROR = 'error', +} + +export enum LOGGING_LEVEL { + error = 1, + warn = 2, + log = 3, + info = 4, + trace = 5, +} + +export enum LOGGER { + ERROR = 'error', + WARN = 'warn', + INFO = 'info', + LOG = 'log', + TRACE = 'trace', +} diff --git a/packages/byods/src/base-client/constant.ts b/packages/byods/src/base-client/constant.ts new file mode 100644 index 00000000000..26f1cf7d04b --- /dev/null +++ b/packages/byods/src/base-client/constant.ts @@ -0,0 +1 @@ +export const BYODS_BASE_CLIENT_FILE = 'base-client'; \ No newline at end of file diff --git a/packages/byods/src/base-client/index.ts b/packages/byods/src/base-client/index.ts index 12b10dca82f..bf599e49a47 100644 --- a/packages/byods/src/base-client/index.ts +++ b/packages/byods/src/base-client/index.ts @@ -3,13 +3,17 @@ import fetch, {Response, RequestInit} from 'node-fetch'; import TokenManager from '../token-manager'; import DataSourceClient from '../data-source-client'; import {HttpClient, ApiResponse} from '../http-client/types'; +import {BYODSConfig} from '../token-manager/type'; +import {BYODS_BASE_CLIENT_FILE} from './constant'; +import log from '../Logger'; +import {LOGGER} from '../Logger/types'; export default class BaseClient { private baseUrl: string; private headers: Record; private tokenManager: TokenManager; private orgId: string; - + private sdkConfig?: BYODSConfig; public dataSource: DataSourceClient; /** @@ -25,13 +29,17 @@ export default class BaseClient { baseUrl: string, headers: Record, tokenManager: TokenManager, - orgId: string + orgId: string, + config?:BYODSConfig ) { this.baseUrl = baseUrl; this.headers = headers; this.tokenManager = tokenManager; this.orgId = orgId; this.dataSource = new DataSourceClient(this.getHttpClientForOrg()); + this.sdkConfig = config; + const logLevel = this.sdkConfig?.logger?.level ? this.sdkConfig.logger.level : LOGGER.ERROR; + log.setLogger(logLevel, BYODS_BASE_CLIENT_FILE); } /** diff --git a/packages/byods/src/base-client/type.ts b/packages/byods/src/base-client/type.ts new file mode 100644 index 00000000000..60146fd2f69 --- /dev/null +++ b/packages/byods/src/base-client/type.ts @@ -0,0 +1,107 @@ +import {LOGGER} from '../Logger/types'; +import {BYODSError} from '../Errors'; + +export interface LoggerConfig { + level: LOGGER; +} + +export interface BYODSConfig { + logger?: LoggerConfig; +} + +export type BYODSErrorEmitterCallback = ( + err: BYODSError, + finalError?: boolean +) => void; + +/** + * An interface for the `CallingClient` module. + * The `CallingClient` module is designed to provide a set of APIs related to line registration and calling functionalities within the SDK. + * + * @example + * ```javascript + * const callingClient = createClient(webex, callingConfig); + * ``` + */ + + /** + * Represents the `mediaEngine for managing media-related operations within the CallingClient. + * The media engine provides access to audio and video devices such as cameras, microphones, and speakers within the media layer. + * + * @public + * @example + * ``` + * const microphones = await callingClient.mediaEngine.Media.getMicrophones(); + * const speakers = await callingClient.mediaEngine.Media.getSpeakers(); + * const cameras = await callingClient.mediaEngine.Media.getCameras(); + * ``` + */ + + /** + * @ignore + */ + getLoggingLevel(); LOGGER; + + /** + * Retrieves details of the line object(s) belonging to a user. + * + * This method gathers all the {@link ILine} objects and organizes them into a dictionary + * where keys represent `lineId`s and values are arrays of {@link ILine} objects registered with + * the `callingClient` + * + * @example + * ```typescript + * const lines = callingClient.getLines(); + * ``` + * The `lines` response object will have `lineId` as its key and + * a list {@link ILine} objects as it's value. + * ``` + * { + * 'lineId1': lineObj1, + * 'lineId2': lineObj2, + * } + * ``` + */ + // getLines(): Record; + + /** + * Retrieves a dictionary of active calls grouped by `lineId`. + * + * This method gathers active {@link ICall} objects and organizes them into a dictionary + * where keys represent `lineId`s and values are arrays of {@link ICall} objects of active calls associated + * with each line. + * + * @example + * ```typescript + * const activeCalls = callingClient.getActiveCalls(); + * ``` + * The `activeCalls` response object will have `lineId` as its key and + * a list {@link ICall} objects as it's value. + * + * ``` + * { + * 'line1': [call1, call2], + * 'line2': [call3], + * } + * ``` + */ + // getActiveCalls(): Record; + + /** + * Retrieves the {@link ICall} object for the currently connected call in the client. + * + * This method iterates through active call objects and returns the call + * that is currently connected (not on hold). + * + * @example + * ```typescript + * const connectedCall : ICall = callingClient.getConnectedCall(); + * ``` + * The `connectedCall` object will be the Call object of the connected call with the client + */ + // getConnectedCall(): ICall | undefined; + +function getLoggingLevel() { + throw new Error('Function not implemented.'); +} + diff --git a/packages/byods/src/byods/constant.ts b/packages/byods/src/byods/constant.ts new file mode 100644 index 00000000000..925eb9601a9 --- /dev/null +++ b/packages/byods/src/byods/constant.ts @@ -0,0 +1 @@ +export const BYODS_FILE = 'byods'; diff --git a/packages/byods/src/byods/index.ts b/packages/byods/src/byods/index.ts index 7f2996ad557..d91ab7e32c3 100644 --- a/packages/byods/src/byods/index.ts +++ b/packages/byods/src/byods/index.ts @@ -10,6 +10,10 @@ import { } from '../constants'; import {SDKConfig} from '../types'; import TokenManager from '../token-manager'; +import {BYODSConfig} from '../token-manager/type'; +import {BYODS_FILE} from './constant'; +import log from '../Logger'; +import {LOGGER} from '../Logger/types'; /** * The BYoDS SDK. @@ -24,6 +28,7 @@ export default class BYODS { private env: 'production' | 'integration'; private config: SDKConfig; private baseUrl: string; + private sdkConfig?: BYODSConfig; /** * The token manager for the SDK. @@ -37,9 +42,12 @@ export default class BYODS { * @example * const sdk = new BYODS({ clientId: 'your-client-id', clientSecret: 'your-client-secret' }); */ - constructor({clientId, clientSecret}: SDKConfig) { + constructor({clientId, clientSecret}: SDKConfig,config?:BYODSConfig) { this.config = {clientId, clientSecret}; this.tokenManager = new TokenManager(clientId, clientSecret); + this.sdkConfig = config; + const logLevel = this.sdkConfig?.logger?.level ? this.sdkConfig.logger.level : LOGGER.ERROR; + log.setLogger(logLevel, BYODS_FILE); /** * The environment variable `process.env.BYODS_ENVIRONMENT` determines the environment in which the SDK operates. diff --git a/packages/byods/src/byods/type.ts b/packages/byods/src/byods/type.ts new file mode 100644 index 00000000000..60146fd2f69 --- /dev/null +++ b/packages/byods/src/byods/type.ts @@ -0,0 +1,107 @@ +import {LOGGER} from '../Logger/types'; +import {BYODSError} from '../Errors'; + +export interface LoggerConfig { + level: LOGGER; +} + +export interface BYODSConfig { + logger?: LoggerConfig; +} + +export type BYODSErrorEmitterCallback = ( + err: BYODSError, + finalError?: boolean +) => void; + +/** + * An interface for the `CallingClient` module. + * The `CallingClient` module is designed to provide a set of APIs related to line registration and calling functionalities within the SDK. + * + * @example + * ```javascript + * const callingClient = createClient(webex, callingConfig); + * ``` + */ + + /** + * Represents the `mediaEngine for managing media-related operations within the CallingClient. + * The media engine provides access to audio and video devices such as cameras, microphones, and speakers within the media layer. + * + * @public + * @example + * ``` + * const microphones = await callingClient.mediaEngine.Media.getMicrophones(); + * const speakers = await callingClient.mediaEngine.Media.getSpeakers(); + * const cameras = await callingClient.mediaEngine.Media.getCameras(); + * ``` + */ + + /** + * @ignore + */ + getLoggingLevel(); LOGGER; + + /** + * Retrieves details of the line object(s) belonging to a user. + * + * This method gathers all the {@link ILine} objects and organizes them into a dictionary + * where keys represent `lineId`s and values are arrays of {@link ILine} objects registered with + * the `callingClient` + * + * @example + * ```typescript + * const lines = callingClient.getLines(); + * ``` + * The `lines` response object will have `lineId` as its key and + * a list {@link ILine} objects as it's value. + * ``` + * { + * 'lineId1': lineObj1, + * 'lineId2': lineObj2, + * } + * ``` + */ + // getLines(): Record; + + /** + * Retrieves a dictionary of active calls grouped by `lineId`. + * + * This method gathers active {@link ICall} objects and organizes them into a dictionary + * where keys represent `lineId`s and values are arrays of {@link ICall} objects of active calls associated + * with each line. + * + * @example + * ```typescript + * const activeCalls = callingClient.getActiveCalls(); + * ``` + * The `activeCalls` response object will have `lineId` as its key and + * a list {@link ICall} objects as it's value. + * + * ``` + * { + * 'line1': [call1, call2], + * 'line2': [call3], + * } + * ``` + */ + // getActiveCalls(): Record; + + /** + * Retrieves the {@link ICall} object for the currently connected call in the client. + * + * This method iterates through active call objects and returns the call + * that is currently connected (not on hold). + * + * @example + * ```typescript + * const connectedCall : ICall = callingClient.getConnectedCall(); + * ``` + * The `connectedCall` object will be the Call object of the connected call with the client + */ + // getConnectedCall(): ICall | undefined; + +function getLoggingLevel() { + throw new Error('Function not implemented.'); +} + diff --git a/packages/byods/src/data-source-client/constants.ts b/packages/byods/src/data-source-client/constants.ts index 2a8462b19c4..efef9e0821d 100644 --- a/packages/byods/src/data-source-client/constants.ts +++ b/packages/byods/src/data-source-client/constants.ts @@ -1 +1,2 @@ export const DATASOURCE_ENDPOINT = '/dataSources'; +export const BYODS_DATA_SOURCE_CLIENT_FILE = 'data-source-client'; diff --git a/packages/byods/src/data-source-client/index.ts b/packages/byods/src/data-source-client/index.ts index c1b5d7abd62..f097d9a0c7a 100644 --- a/packages/byods/src/data-source-client/index.ts +++ b/packages/byods/src/data-source-client/index.ts @@ -1,12 +1,17 @@ import {DataSourceRequest, DataSourceResponse} from './types'; import {DATASOURCE_ENDPOINT} from './constants'; import {HttpClient, ApiResponse} from '../http-client/types'; +import {BYODSConfig} from '../token-manager/type'; +import {BYODS_DATA_SOURCE_CLIENT_FILE} from '../data-source-client/constants'; +import log from '../Logger'; +import {LOGGER} from '../Logger/types'; /** * Client for interacting with the /dataSource API. */ export default class DataSourceClient { private httpClient: HttpClient; + private sdkConfig?: BYODSConfig; /** * Creates an instance of DataSourceClient. @@ -15,8 +20,11 @@ export default class DataSourceClient { * const httpClient = new HttpClient(); * const client = new DataSourceClient(httpClient); */ - constructor(httpClient: HttpClient) { + constructor(httpClient: HttpClient,config?:BYODSConfig) { this.httpClient = httpClient; + this.sdkConfig = config; + const logLevel = this.sdkConfig?.logger?.level ? this.sdkConfig.logger.level : LOGGER.ERROR; + log.setLogger(logLevel, BYODS_DATA_SOURCE_CLIENT_FILE); } /** diff --git a/packages/byods/src/data-source-client/types.ts b/packages/byods/src/data-source-client/types.ts index 923b0af6c2b..9005d9e83d8 100644 --- a/packages/byods/src/data-source-client/types.ts +++ b/packages/byods/src/data-source-client/types.ts @@ -1,3 +1,28 @@ +import {LOGGER} from '../Logger/types'; +import {BYODSError} from '../Errors'; + +export interface LoggerConfig { + level: LOGGER; +} + +export interface BYODSConfig { + logger?: LoggerConfig; +} + +export type BYODSErrorEmitterCallback = ( + err: BYODSError, + finalError?: boolean +) => void; + + /** + * @ignore + */ + getLoggingLevel(); LOGGER; + + function getLoggingLevel() { + throw new Error('Function not implemented.'); + } + /** * Represents the response from a data source. * @@ -58,6 +83,7 @@ export interface DataSourceResponse { * The error message associated with the data source response, if any. */ errorMessage?: string; + } /** @@ -96,3 +122,4 @@ export interface DataSourceRequest { */ tokenLifetimeMinutes: number; } + diff --git a/packages/byods/src/token-manager/constant.ts b/packages/byods/src/token-manager/constant.ts new file mode 100644 index 00000000000..1a80b45c609 --- /dev/null +++ b/packages/byods/src/token-manager/constant.ts @@ -0,0 +1 @@ +export const BYODS_TOKEN_MANAGER_FILE = 'token-manager'; \ No newline at end of file diff --git a/packages/byods/src/token-manager/index.ts b/packages/byods/src/token-manager/index.ts index c88de4ecc7c..558355489c6 100644 --- a/packages/byods/src/token-manager/index.ts +++ b/packages/byods/src/token-manager/index.ts @@ -1,7 +1,12 @@ import fetch, {Response} from 'node-fetch'; - +import log from '../Logger'; +import {LOGGER} from '../Logger/types'; import {APPLICATION_ID_PREFIX, PRODUCTION_BASE_URL} from '../constants'; import {TokenResponse, OrgServiceAppAuthorization, ServiceAppAuthorizationMap} from '../types'; +import ExtendedError from 'Errors/catalog/ExtendedError'; +import { ERROR_TYPE } from 'Errors/types'; +import {BYODSConfig} from '../token-manager/type'; +import { BYODS_TOKEN_MANAGER_FILE } from './constant'; /** * The token manager for the BYoDS SDK. @@ -12,6 +17,7 @@ export default class TokenManager { private clientSecret: string; private serviceAppId: string; private baseUrl: string; + private sdkConfig?: BYODSConfig; /** * Creates an instance of TokenManager. @@ -22,14 +28,17 @@ export default class TokenManager { * @example * const tokenManager = new TokenManager('your-client-id', 'your-client-secret'); */ - constructor(clientId: string, clientSecret: string, baseUrl: string = PRODUCTION_BASE_URL) { + constructor(clientId: string, clientSecret: string, baseUrl: string = PRODUCTION_BASE_URL,config?:BYODSConfig) { if (!clientId || !clientSecret) { throw new Error('clientId and clientSecret are required'); } this.clientId = clientId; this.clientSecret = clientSecret; this.baseUrl = baseUrl; + this.sdkConfig = config; this.serviceAppId = Buffer.from(`${APPLICATION_ID_PREFIX}${clientId}`).toString('base64'); + const logLevel = this.sdkConfig?.logger?.level ? this.sdkConfig.logger.level : LOGGER.ERROR; + log.setLogger(logLevel, BYODS_TOKEN_MANAGER_FILE); } /** @@ -114,7 +123,12 @@ export default class TokenManager { const data: TokenResponse = (await response.json()) as TokenResponse; this.updateServiceAppToken(data, orgId); } catch (error) { - console.error('Error retrieving token after authorization:', error); + log.error( + new ExtendedError( + 'Error retrieving token after authorization', ERROR_TYPE.REGISTRATION_ERROR + ),{ file: 'BYODS_TOKEN_MANAGER_FILE', method: 'getServiceAppTokenUsingPAT' } + ); + throw error; } } @@ -179,7 +193,12 @@ export default class TokenManager { const data: TokenResponse = (await response.json()) as TokenResponse; this.updateServiceAppToken(data, orgId); } catch (error) { - console.error('Error saving service app registration:', error); + log.error( + new ExtendedError( + 'Error saving service app registration', ERROR_TYPE.REGISTRATION_ERROR + ),{ file: 'BYODS_TOKEN_MANAGER_FILE', method: 'saveServiceAppRegistration' } + ); + throw error; } } diff --git a/packages/byods/src/token-manager/type.ts b/packages/byods/src/token-manager/type.ts new file mode 100644 index 00000000000..60146fd2f69 --- /dev/null +++ b/packages/byods/src/token-manager/type.ts @@ -0,0 +1,107 @@ +import {LOGGER} from '../Logger/types'; +import {BYODSError} from '../Errors'; + +export interface LoggerConfig { + level: LOGGER; +} + +export interface BYODSConfig { + logger?: LoggerConfig; +} + +export type BYODSErrorEmitterCallback = ( + err: BYODSError, + finalError?: boolean +) => void; + +/** + * An interface for the `CallingClient` module. + * The `CallingClient` module is designed to provide a set of APIs related to line registration and calling functionalities within the SDK. + * + * @example + * ```javascript + * const callingClient = createClient(webex, callingConfig); + * ``` + */ + + /** + * Represents the `mediaEngine for managing media-related operations within the CallingClient. + * The media engine provides access to audio and video devices such as cameras, microphones, and speakers within the media layer. + * + * @public + * @example + * ``` + * const microphones = await callingClient.mediaEngine.Media.getMicrophones(); + * const speakers = await callingClient.mediaEngine.Media.getSpeakers(); + * const cameras = await callingClient.mediaEngine.Media.getCameras(); + * ``` + */ + + /** + * @ignore + */ + getLoggingLevel(); LOGGER; + + /** + * Retrieves details of the line object(s) belonging to a user. + * + * This method gathers all the {@link ILine} objects and organizes them into a dictionary + * where keys represent `lineId`s and values are arrays of {@link ILine} objects registered with + * the `callingClient` + * + * @example + * ```typescript + * const lines = callingClient.getLines(); + * ``` + * The `lines` response object will have `lineId` as its key and + * a list {@link ILine} objects as it's value. + * ``` + * { + * 'lineId1': lineObj1, + * 'lineId2': lineObj2, + * } + * ``` + */ + // getLines(): Record; + + /** + * Retrieves a dictionary of active calls grouped by `lineId`. + * + * This method gathers active {@link ICall} objects and organizes them into a dictionary + * where keys represent `lineId`s and values are arrays of {@link ICall} objects of active calls associated + * with each line. + * + * @example + * ```typescript + * const activeCalls = callingClient.getActiveCalls(); + * ``` + * The `activeCalls` response object will have `lineId` as its key and + * a list {@link ICall} objects as it's value. + * + * ``` + * { + * 'line1': [call1, call2], + * 'line2': [call3], + * } + * ``` + */ + // getActiveCalls(): Record; + + /** + * Retrieves the {@link ICall} object for the currently connected call in the client. + * + * This method iterates through active call objects and returns the call + * that is currently connected (not on hold). + * + * @example + * ```typescript + * const connectedCall : ICall = callingClient.getConnectedCall(); + * ``` + * The `connectedCall` object will be the Call object of the connected call with the client + */ + // getConnectedCall(): ICall | undefined; + +function getLoggingLevel() { + throw new Error('Function not implemented.'); +} + diff --git a/packages/byods/test/unit/spec/Logger/index.text.ts b/packages/byods/test/unit/spec/Logger/index.text.ts new file mode 100644 index 00000000000..b4da3862ded --- /dev/null +++ b/packages/byods/test/unit/spec/Logger/index.text.ts @@ -0,0 +1,77 @@ +import ExtendedError from '../../../../src/Errors/catalog/ExtendedError'; +import {LOGGER} from '../../../../src/Logger/types'; +import log from '../../../../src/Logger'; + +describe('Coverage tests for logger', () => { + let logLevel: LOGGER; + + const logSpy = jest.spyOn(console, 'log'); + const traceSpy = jest.spyOn(console, 'trace'); + const warnSpy = jest.spyOn(console, 'warn'); + const errorSpy = jest.spyOn(console, 'error'); + + beforeEach(() => { + logLevel = LOGGER.ERROR; + }); + + const fakePrint = 'Example log statement'; + const dummyContext = { + file: 'logger.test.ts', + method: 'dummy', + }; + + it('Set the log level to error and verify that all levels are not executed except error', () => { + log.info(fakePrint, dummyContext); + expect(logSpy).not.toHaveBeenCalledTimes(1); + + log.log(fakePrint, dummyContext); + expect(logSpy).not.toHaveBeenCalledTimes(1); + + log.warn(fakePrint, dummyContext); + expect(warnSpy).not.toHaveBeenCalledTimes(1); + + log.trace(fakePrint, dummyContext); + expect(traceSpy).not.toHaveBeenCalledTimes(1); + + log.error(new Error(fakePrint) as ExtendedError, dummyContext); + expect(errorSpy).toHaveBeenCalledTimes(1); + }); + + it('Set the logger and verify the level', () => { + expect(logLevel).toStrictEqual(LOGGER.ERROR); + log.setLogger(LOGGER.TRACE); + expect(log.getLogLevel()).toStrictEqual(LOGGER.TRACE); + }); + + it('Set the log level to Info and verify levels below info are executed or not', () => { + log.setLogger(LOGGER.INFO); + + log.info(fakePrint, dummyContext); + expect(logSpy).toHaveBeenCalledTimes(2); + + log.log(fakePrint, dummyContext); + expect(logSpy).toHaveBeenCalledTimes(3); + + log.warn(fakePrint, dummyContext); + expect(warnSpy).toHaveBeenCalledTimes(1); + + log.trace(fakePrint, dummyContext); + expect(traceSpy).not.toHaveBeenCalledTimes(1); + }); + + it('Set the log level to Trace and verify that all levels are executed', () => { + log.setLogger(LOGGER.TRACE); + + log.info(fakePrint, dummyContext); + expect(logSpy).toHaveBeenCalledTimes(2); // one during initialization and one with the statement + + log.log(fakePrint, dummyContext); + expect(logSpy).toHaveBeenCalledTimes(3); // +1 because both info and log internally use console.log + + log.warn(fakePrint, dummyContext); + expect(warnSpy).toHaveBeenCalledTimes(1); + + log.trace(fakePrint, dummyContext); + expect(traceSpy).toHaveBeenCalledTimes(1); + }); +});