diff --git a/README.md b/README.md index 85b2ffce..ca3c189c 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,14 @@ Official JavaScript SDK for [Deepgram](https://www.deepgram.com/). Power your ap - [Initialization](#initialization) - [Getting an API Key](#getting-an-api-key) - [Scoped Configuration](#scoped-configuration) - - [Rest requests in the browser](#rest-requests-in-the-browser) + - [1. Global Defaults](#1-global-defaults) + - [2. Namespace-specific Configurations](#2-namespace-specific-configurations) + - [3. Transport Options](#3-transport-options) + - [Change the API url used for all SDK methods](#change-the-api-url-used-for-all-sdk-methods) + - [Change the API url used for transcription only](#change-the-api-url-used-for-transcription-only) + - [Override fetch transmitter](#override-fetch-transmitter) + - [Proxy requests in the browser](#proxy-requests-in-the-browser) + - [Set custom headers for fetch](#set-custom-headers-for-fetch) - [Transcription (Synchronous)](#transcription-synchronous) - [Remote Files](#remote-files) - [Local Files](#local-files) @@ -55,7 +62,7 @@ Official JavaScript SDK for [Deepgram](https://www.deepgram.com/). Power your ap - [Get On-Prem credentials](#get-on-prem-credentials) - [Create On-Prem credentials](#create-on-prem-credentials) - [Delete On-Prem credentials](#delete-on-prem-credentials) -- [Backwards Compatibility](#backwards-compatibility) +- [Backwards Compatibility](#backwards-compatibility) - [Development and Contributing](#development-and-contributing) - [Debugging and making changes locally](#debugging-and-making-changes-locally) - [Getting Help](#getting-help) @@ -130,20 +137,74 @@ const deepgram = createClient(DEEPGRAM_API_KEY); # Scoped Configuration -A new feature is scoped configuration. You'll be able to configure various aspects of the SDK from the initialization. +The SDK supports scoped configurtion. You'll be able to configure various aspects of each namespace of the SDK from the initialization. Below outlines a flexible and customizable configuration system for the Deepgram SDK. Here’s how the namespace configuration works: + +## 1. Global Defaults + +- The `global` namespace serves as the foundational configuration applicable across all other namespaces unless overridden. +- Includes general settings like URL and headers applicable for all API calls. +- If no specific configurations are provided for other namespaces, the `global` defaults are used. + +## 2. Namespace-specific Configurations + +- Each namespace (`listen`, `manage`, `onprem`, `read`, `speak`) can have its specific configurations which override the `global` settings within their respective scopes. +- Allows for detailed control over different parts of the application interacting with various Deepgram API endpoints. + +## 3. Transport Options + +- Configurations for both `fetch` and `websocket` can be specified under each namespace, allowing different transport mechanisms for different operations. +- For example, the `fetch` configuration can have its own URL and proxy settings distinct from the `websocket`. +- The generic interfaces define a structure for transport options which include a client (like a `fetch` or `WebSocket` instance) and associated options (like headers, URL, proxy settings). + +This configuration system enables robust customization where defaults provide a foundation, but every aspect of the client's interaction with the API can be finely controlled and tailored to specific needs through namespace-specific settings. This enhances the maintainability and scalability of the application by localizing configurations to their relevant contexts. + +## Change the API url used for all SDK methods + +Useful for using different API environments (for e.g. beta). + +```js +import { createClient } from "@deepgram/sdk"; +// - or - +// const { createClient } = require("@deepgram/sdk"); + +const deepgram = createClient(DEEPGRAM_API_KEY, { + global: { fetch: { options: { url: "https://api.beta.deepgram.com" } } }, +}); +``` + +## Change the API url used for transcription only + +Useful for on-prem installations. Only affects requests to `/listen` endpoints. + +```js +import { createClient } from "@deepgram/sdk"; +// - or - +// const { createClient } = require("@deepgram/sdk"); + +const deepgram = createClient(DEEPGRAM_API_KEY, { + listen: { fetch: { options: { url: "http://localhost:8080" } } }, +}); +``` + +## Override fetch transmitter + +Useful for providing a custom http client. ```js import { createClient } from "@deepgram/sdk"; // - or - // const { createClient } = require("@deepgram/sdk"); +const yourFetch = async () => { + return Response("...etc"); +}; + const deepgram = createClient(DEEPGRAM_API_KEY, { - global: { url: "https://api.beta.deepgram.com" }, - // restProxy: { url: "http://localhost:8080" } + global: { fetch: { client: yourFetch } }, }); ``` -## Rest requests in the browser +## Proxy requests in the browser This SDK now works in the browser. If you'd like to make REST-based requests (pre-recorded transcription, on-premise, and management requests), then you'll need to use a proxy as we do not support custom CORS origins on our API. To set up your proxy, you configure the SDK like so: @@ -151,7 +212,7 @@ This SDK now works in the browser. If you'd like to make REST-based requests (pr import { createClient } from "@deepgram/sdk"; const deepgram = createClient("proxy", { - restProxy: { url: "http://localhost:8080" }, + global: { fetch: { options: { proxy: { url: "http://localhost:8080" } } } }, }); ``` @@ -161,6 +222,18 @@ Your proxy service should replace the Authorization header with `Authorization: Check out our example Node-based proxy here: [Deepgram Node Proxy](https://github.com/deepgram-devs/deepgram-node-proxy). +## Set custom headers for fetch + +Useful for many things. + +```js +import { createClient } from "@deepgram/sdk"; + +const deepgram = createClient("proxy", { + global: { fetch: { options: { headers: { "x-custom-header": "foo" } } } }, +}); +``` + # Transcription (Synchronous) ## Remote Files @@ -562,6 +635,8 @@ const { result, error } = await deepgram.onprem.deleteCredentials(projectId, cre Older SDK versions will receive Priority 1 (P1) bug support only. Security issues, both in our code and dependencies, are promptly addressed. Significant bugs without clear workarounds are also given priority attention. +We strictly follow semver, and will not introduce breaking changes to the publicly documented interfaces of the SDK. Use internal and undocumented interfaces without pinning your version, at your own risk. + # Development and Contributing Interested in contributing? We ❤️ pull requests! diff --git a/examples/browser-live/index.html b/examples/browser-live/index.html new file mode 100644 index 00000000..6cb98002 --- /dev/null +++ b/examples/browser-live/index.html @@ -0,0 +1,31 @@ + + + + + + + + Running test... check the developer console. + + diff --git a/examples/browser-prerecorded/index.html b/examples/browser-prerecorded/index.html new file mode 100644 index 00000000..f3b31e6c --- /dev/null +++ b/examples/browser-prerecorded/index.html @@ -0,0 +1,53 @@ + + + + + + + + Running test... check the developer console. + + diff --git a/package-lock.json b/package-lock.json index f71e9bf3..bbdbc9bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,11 +10,10 @@ "license": "MIT", "dependencies": { "@deepgram/captions": "^1.1.1", - "@types/websocket": "^1.0.9", "cross-fetch": "^3.1.5", "deepmerge": "^4.3.1", "events": "^3.3.0", - "websocket": "^1.0.34" + "ws": "^8.17.0" }, "devDependencies": { "@commitlint/cli": "^17.6.7", @@ -23,6 +22,8 @@ "@flydotio/dockerfile": "^0.4.10", "@types/chai": "^4.3.5", "@types/mocha": "^9.1.1", + "@types/sinon": "^17.0.3", + "@types/ws": "^8.5.10", "chai": "^4.3.7", "cross-env": "^7.0.3", "husky": "^4.3.0", @@ -34,6 +35,7 @@ "pretty-quick": "^3.1.3", "rimraf": "^3.0.2", "semantic-release-plugin-update-version-in-files": "^1.1.0", + "sinon": "^17.0.1", "ts-loader": "^8.0.11", "ts-node": "^10.9.1", "typedoc": "^0.22.16", @@ -1072,6 +1074,50 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", + "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@sinonjs/samsam": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", + "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^2.0.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "node_modules/@sinonjs/samsam/node_modules/@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", + "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", + "dev": true + }, "node_modules/@tsconfig/node10": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", @@ -1155,7 +1201,8 @@ "node_modules/@types/node": { "version": "14.18.53", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.53.tgz", - "integrity": "sha512-soGmOpVBUq+gaBMwom1M+krC/NNbWlosh4AtGA03SyWNDiqSKtwp7OulO1M6+mg8YkHMvJ/y0AkCeO8d1hNb7A==" + "integrity": "sha512-soGmOpVBUq+gaBMwom1M+krC/NNbWlosh4AtGA03SyWNDiqSKtwp7OulO1M6+mg8YkHMvJ/y0AkCeO8d1hNb7A==", + "dev": true }, "node_modules/@types/normalize-package-data": { "version": "2.4.1", @@ -1169,10 +1216,26 @@ "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", "dev": true }, - "node_modules/@types/websocket": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@types/websocket/-/websocket-1.0.9.tgz", - "integrity": "sha512-xrMBdqdKdlE+7L9Wg2PQblIkZGSgiMlEoP6UAaYKMHbbxqCJ6PV/pTZ2RcMcSSERurU2TtGbmO4lqpFOJd01ww==", + "node_modules/@types/sinon": { + "version": "17.0.3", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.3.tgz", + "integrity": "sha512-j3uovdn8ewky9kRBG19bOwaZbexJu/XjtkHyjvUgt4xfPFz18dcORIMqnYh66Fx3Powhcr85NT5+er3+oViapw==", + "dev": true, + "dependencies": { + "@types/sinonjs__fake-timers": "*" + } + }, + "node_modules/@types/sinonjs__fake-timers": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz", + "integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==", + "dev": true + }, + "node_modules/@types/ws": { + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", + "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==", + "dev": true, "dependencies": { "@types/node": "*" } @@ -1706,6 +1769,8 @@ "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.8.tgz", "integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==", "hasInstallScript": true, + "optional": true, + "peer": true, "dependencies": { "node-gyp-build": "^4.3.0" }, @@ -2168,15 +2233,6 @@ "node": ">= 8" } }, - "node_modules/d": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", - "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", - "dependencies": { - "es5-ext": "^0.10.50", - "type": "^1.0.1" - } - }, "node_modules/dargs": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/dargs/-/dargs-7.0.0.tgz", @@ -2539,46 +2595,12 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/es5-ext": { - "version": "0.10.64", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", - "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", - "hasInstallScript": true, - "dependencies": { - "es6-iterator": "^2.0.3", - "es6-symbol": "^3.1.3", - "esniff": "^2.0.1", - "next-tick": "^1.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, "node_modules/es6-error": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", "dev": true }, - "node_modules/es6-iterator": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", - "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", - "dependencies": { - "d": "1", - "es5-ext": "^0.10.35", - "es6-symbol": "^3.1.1" - } - }, - "node_modules/es6-symbol": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", - "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", - "dependencies": { - "d": "^1.0.1", - "ext": "^1.1.2" - } - }, "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -2610,25 +2632,6 @@ "node": ">=8.0.0" } }, - "node_modules/esniff": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", - "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", - "dependencies": { - "d": "^1.0.1", - "es5-ext": "^0.10.62", - "event-emitter": "^0.3.5", - "type": "^2.7.2" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esniff/node_modules/type": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/type/-/type-2.7.2.tgz", - "integrity": "sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==" - }, "node_modules/esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", @@ -2672,15 +2675,6 @@ "node": ">=4.0" } }, - "node_modules/event-emitter": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", - "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", - "dependencies": { - "d": "1", - "es5-ext": "~0.10.14" - } - }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -2724,19 +2718,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ext": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", - "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", - "dependencies": { - "type": "^2.7.2" - } - }, - "node_modules/ext/node_modules/type": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/type/-/type-2.7.2.tgz", - "integrity": "sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==" - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3736,7 +3717,8 @@ "node_modules/is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true }, "node_modules/is-unicode-supported": { "version": "0.1.0", @@ -4070,6 +4052,12 @@ "node": "*" } }, + "node_modules/just-extend": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", + "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", + "dev": true + }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -4178,6 +4166,12 @@ "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==", "dev": true }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true + }, "node_modules/lodash.isfunction": { "version": "3.0.9", "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz", @@ -4647,17 +4641,25 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, - "node_modules/next-tick": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", - "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==" - }, "node_modules/nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, + "node_modules/nise": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.9.tgz", + "integrity": "sha512-qOnoujW4SV6e40dYxJOb3uvuoPHtmLzIk4TFo+j0jPJoC+5Z9xja5qH5JZobEPsa8+YYphMrOSwnrshEhG2qww==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/text-encoding": "^0.7.2", + "just-extend": "^6.2.0", + "path-to-regexp": "^6.2.1" + } + }, "node_modules/node-fetch": { "version": "2.6.12", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz", @@ -4681,6 +4683,8 @@ "version": "4.7.0", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.7.0.tgz", "integrity": "sha512-PbZERfeFdrHQOOXiAKOY0VPbykZy90ndPKk0d+CFDegTKmWp1VgOTz2xACVbr1BjCWxrQp68CXtvNsveFhqDJg==", + "optional": true, + "peer": true, "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", @@ -5378,6 +5382,12 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "node_modules/path-to-regexp": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.2.tgz", + "integrity": "sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==", + "dev": true + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -6329,6 +6339,54 @@ "node": ">=10" } }, + "node_modules/sinon": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-17.0.1.tgz", + "integrity": "sha512-wmwE19Lie0MLT+ZYNpDymasPHUKTaZHUH/pKEubRXIzySv9Atnlw+BUMGCzWgV7b7wO+Hw6f1TEOr0IUnmU8/g==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/samsam": "^8.0.0", + "diff": "^5.1.0", + "nise": "^5.1.5", + "supports-color": "^7.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, + "node_modules/sinon/node_modules/diff": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/sinon/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/sinon/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -6875,11 +6933,6 @@ "node": ">=0.3.1" } }, - "node_modules/type": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", - "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==" - }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -6970,6 +7023,7 @@ "version": "3.1.5", "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, "dependencies": { "is-typedarray": "^1.0.0" } @@ -7093,6 +7147,8 @@ "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", "hasInstallScript": true, + "optional": true, + "peer": true, "dependencies": { "node-gyp-build": "^4.3.0" }, @@ -7292,35 +7348,6 @@ "node": ">=10.13.0" } }, - "node_modules/websocket": { - "version": "1.0.34", - "resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.34.tgz", - "integrity": "sha512-PRDso2sGwF6kM75QykIesBijKSVceR6jL2G8NGYyq2XrItNC2P5/qL5XeR056GhA+Ly7JMFvJb9I312mJfmqnQ==", - "dependencies": { - "bufferutil": "^4.0.1", - "debug": "^2.2.0", - "es5-ext": "^0.10.50", - "typedarray-to-buffer": "^3.1.5", - "utf-8-validate": "^5.0.2", - "yaeti": "^0.0.6" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/websocket/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/websocket/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -7475,6 +7502,26 @@ "typedarray-to-buffer": "^3.1.5" } }, + "node_modules/ws": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz", + "integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -7484,14 +7531,6 @@ "node": ">=10" } }, - "node_modules/yaeti": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz", - "integrity": "sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug==", - "engines": { - "node": ">=0.10.32" - } - }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", diff --git a/package.json b/package.json index a0daf61d..a0c43732 100644 --- a/package.json +++ b/package.json @@ -45,18 +45,17 @@ "build:module": "tsc -p tsconfig.module.json", "build:umd": "webpack --mode=production", "watch": "nodemon -e ts --watch src --exec \"npm run build\"", - "test": "mocha -r ts-node/register test/*test.ts test/**/*test.ts --insect --timeout 5000 || :", + "test": "mocha -r ts-node/register test/*test.ts test/**/*test.ts --inspect --exit", "test:coverage": "nyc --reporter=lcovonly --reporter=text --reporter=text-summary npm run test", "docs": "typedoc --entryPoints src/index.ts --out docs/v2 --includes src/**/*.ts", "docs:json": "typedoc --entryPoints src/index.ts --includes src/**/*.ts --json docs/v2/spec.json --excludeExternals" }, "dependencies": { "@deepgram/captions": "^1.1.1", - "@types/websocket": "^1.0.9", "cross-fetch": "^3.1.5", "deepmerge": "^4.3.1", "events": "^3.3.0", - "websocket": "^1.0.34" + "ws": "^8.17.0" }, "devDependencies": { "@commitlint/cli": "^17.6.7", @@ -65,6 +64,8 @@ "@flydotio/dockerfile": "^0.4.10", "@types/chai": "^4.3.5", "@types/mocha": "^9.1.1", + "@types/sinon": "^17.0.3", + "@types/ws": "^8.5.10", "chai": "^4.3.7", "cross-env": "^7.0.3", "husky": "^4.3.0", @@ -76,6 +77,7 @@ "pretty-quick": "^3.1.3", "rimraf": "^3.0.2", "semantic-release-plugin-update-version-in-files": "^1.1.0", + "sinon": "^17.0.1", "ts-loader": "^8.0.11", "ts-node": "^10.9.1", "typedoc": "^0.22.16", diff --git a/src/DeepgramClient.ts b/src/DeepgramClient.ts index 216abd3a..7b0fa04e 100644 --- a/src/DeepgramClient.ts +++ b/src/DeepgramClient.ts @@ -1,71 +1,99 @@ import { DeepgramVersionError } from "./lib/errors"; -import { AbstractClient } from "./packages/AbstractClient"; -import { ListenClient } from "./packages/ListenClient"; -import { ManageClient } from "./packages/ManageClient"; -import { OnPremClient } from "./packages/OnPremClient"; -import { ReadClient } from "./packages/ReadClient"; -import { SpeakClient } from "./packages/SpeakClient"; +import { + AbstractClient, + ListenClient, + ManageClient, + OnPremClient, + ReadClient, + SpeakClient, +} from "./packages"; /** - * Deepgram Client. + * The DeepgramClient class provides access to various Deepgram API clients, including ListenClient, ManageClient, OnPremClient, ReadClient, and SpeakClient. * - * An isomorphic Javascript client for interacting with the Deepgram API. - * @see https://developers.deepgram.com/docs/js-sdk + * @see https://github.com/deepgram/deepgram-js-sdk */ export default class DeepgramClient extends AbstractClient { get listen(): ListenClient { - return new ListenClient(this.key, this.options); + return new ListenClient(this.options); } get manage(): ManageClient { - return new ManageClient(this.key, this.options); + return new ManageClient(this.options); } get onprem(): OnPremClient { - return new OnPremClient(this.key, this.options); + return new OnPremClient(this.options); } get read(): ReadClient { - return new ReadClient(this.key, this.options); + return new ReadClient(this.options); } get speak(): SpeakClient { - return new SpeakClient(this.key, this.options); + return new SpeakClient(this.options); } /** - * Major version fallback errors are below - * - * @see https://developers.deepgram.com/docs/js-sdk-v2-to-v3-migration-guide + * @deprecated + * @see https://dpgr.am/js-v3 */ get transcription(): any { throw new DeepgramVersionError(); } + /** + * @deprecated + * @see https://dpgr.am/js-v3 + */ get projects(): any { throw new DeepgramVersionError(); } + /** + * @deprecated + * @see https://dpgr.am/js-v3 + */ get keys(): any { throw new DeepgramVersionError(); } + /** + * @deprecated + * @see https://dpgr.am/js-v3 + */ get members(): any { throw new DeepgramVersionError(); } + /** + * @deprecated + * @see https://dpgr.am/js-v3 + */ get scopes(): any { throw new DeepgramVersionError(); } + /** + * @deprecated + * @see https://dpgr.am/js-v3 + */ get invitation(): any { throw new DeepgramVersionError(); } + /** + * @deprecated + * @see https://dpgr.am/js-v3 + */ get usage(): any { throw new DeepgramVersionError(); } + /** + * @deprecated + * @see https://dpgr.am/js-v3 + */ get billing(): any { throw new DeepgramVersionError(); } diff --git a/src/index.ts b/src/index.ts index 7c70a48b..0c2925fe 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,12 @@ -import DeepgramClient from "./DeepgramClient"; import { DeepgramVersionError } from "./lib/errors"; -import type { DeepgramClientOptions } from "./lib/types"; +import DeepgramClient from "./DeepgramClient"; +import type { DeepgramClientOptions, IKeyFactory } from "./lib/types"; /** - * Major version fallback error + * This class is deprecated and should not be used. It throws a `DeepgramVersionError` when instantiated. + * + * @deprecated + * @see https://dpgr.am/js-v3 */ class Deepgram { constructor(protected apiKey: string, protected apiUrl?: string, protected requireSSL?: boolean) { @@ -12,11 +15,36 @@ class Deepgram { } /** - * Creates a new Deepgram Client. + * Creates a new Deepgram client instance. + * + * @param {DeepgramClientArgs} args - Arguments to pass to the Deepgram client constructor. + * @returns A new Deepgram client instance. */ -const createClient = (apiKey: string, options: DeepgramClientOptions = {}): DeepgramClient => { - return new DeepgramClient(apiKey, options); -}; +// Constructor overloads +function createClient(): DeepgramClient; +function createClient(key?: string | IKeyFactory): DeepgramClient; +function createClient(options?: DeepgramClientOptions): DeepgramClient; +function createClient(key?: string | IKeyFactory, options?: DeepgramClientOptions): DeepgramClient; + +// Constructor implementation +function createClient( + keyOrOptions?: string | IKeyFactory | DeepgramClientOptions, + options?: DeepgramClientOptions +): DeepgramClient { + let resolvedOptions: DeepgramClientOptions = {}; + + if (typeof keyOrOptions === "string" || typeof keyOrOptions === "function") { + if (typeof options === "object") { + resolvedOptions = options; + } + + resolvedOptions.key = keyOrOptions; + } else if (typeof keyOrOptions === "object") { + resolvedOptions = keyOrOptions; + } + + return new DeepgramClient(resolvedOptions); +} export { createClient, DeepgramClient, Deepgram }; diff --git a/src/lib/constants.ts b/src/lib/constants.ts index f19cb922..de6714c5 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -1,7 +1,6 @@ -import { isBrowser } from "./helpers"; -import { DeepgramClientOptions } from "./types/DeepgramClientOptions"; -import { FetchOptions } from "./types/Fetch"; +import { convertProtocolToWs, isBrowser } from "./helpers"; import { version } from "./version"; +import type { DefaultNamespaceOptions, DefaultClientOptions } from "./types"; export const NODE_VERSION = process.versions.node; @@ -13,15 +12,27 @@ export const DEFAULT_HEADERS = { export const DEFAULT_URL = "https://api.deepgram.com"; -export const DEFAULT_GLOBAL_OPTIONS = { - url: DEFAULT_URL, +export const DEFAULT_GLOBAL_OPTIONS: Partial = { + fetch: { options: { url: DEFAULT_URL, headers: DEFAULT_HEADERS } }, + websocket: { + options: { url: convertProtocolToWs(DEFAULT_URL), _nodeOnlyHeaders: DEFAULT_HEADERS }, + }, }; -export const DEFAULT_FETCH_OPTIONS: FetchOptions = { - headers: DEFAULT_HEADERS, -}; - -export const DEFAULT_OPTIONS: DeepgramClientOptions = { +export const DEFAULT_OPTIONS: DefaultClientOptions = { global: DEFAULT_GLOBAL_OPTIONS, - fetch: DEFAULT_FETCH_OPTIONS, }; + +export enum SOCKET_STATES { + connecting = 0, + open = 1, + closing = 2, + closed = 3, +} + +export enum CONNECTION_STATE { + Connecting = "connecting", + Open = "open", + Closing = "closing", + Closed = "closed", +} diff --git a/src/lib/enums/LiveConnectionState.ts b/src/lib/enums/LiveConnectionState.ts index 11538068..2d604871 100644 --- a/src/lib/enums/LiveConnectionState.ts +++ b/src/lib/enums/LiveConnectionState.ts @@ -1,6 +1,11 @@ +import { SOCKET_STATES } from "../constants"; + +/** + * @deprecated Since 3.4. use SOCKET_STATES for generic socket connection states. + */ export enum LiveConnectionState { - CONNECTING = 0, - OPEN = 1, - CLOSING = 2, - CLOSED = 3, + CONNECTING = SOCKET_STATES.connecting, + OPEN = SOCKET_STATES.open, + CLOSING = SOCKET_STATES.closing, + CLOSED = SOCKET_STATES.closed, } diff --git a/src/lib/enums/LiveTranscriptionEvents.ts b/src/lib/enums/LiveTranscriptionEvents.ts index cad0323c..06699771 100644 --- a/src/lib/enums/LiveTranscriptionEvents.ts +++ b/src/lib/enums/LiveTranscriptionEvents.ts @@ -1,10 +1,21 @@ export enum LiveTranscriptionEvents { + /** + * Built in socket events. + */ Open = "open", Close = "close", - Transcript = "Results", // exact match to data type from API - Metadata = "Metadata", // exact match to data type from API Error = "error", - Warning = "warning", - UtteranceEnd = "UtteranceEnd", // exact match to data type from API + + /** + * Message { type: string } + */ + Transcript = "Results", + Metadata = "Metadata", + UtteranceEnd = "UtteranceEnd", SpeechStarted = "SpeechStarted", + + /** + * Catch all for any other message event + */ + Unhandled = "Unhandled", } diff --git a/src/lib/enums/index.ts b/src/lib/enums/index.ts index 9c6ba0f6..42226c91 100644 --- a/src/lib/enums/index.ts +++ b/src/lib/enums/index.ts @@ -1,2 +1,2 @@ -export { LiveConnectionState } from "./LiveConnectionState"; -export { LiveTranscriptionEvents } from "./LiveTranscriptionEvents"; +export * from "./LiveConnectionState"; +export * from "./LiveTranscriptionEvents"; diff --git a/src/lib/fetch.ts b/src/lib/fetch.ts index 37f7e594..8212f0a4 100644 --- a/src/lib/fetch.ts +++ b/src/lib/fetch.ts @@ -1,6 +1,6 @@ -import crossFetch from "cross-fetch"; import { resolveHeadersConstructor } from "./helpers"; -import type { Fetch } from "./types/Fetch"; +import crossFetch from "cross-fetch"; +import type { Fetch } from "./types"; export const resolveFetch = (customFetch?: Fetch): Fetch => { let _fetch: Fetch; diff --git a/src/lib/helpers.ts b/src/lib/helpers.ts index 8e18f190..d1809fbb 100644 --- a/src/lib/helpers.ts +++ b/src/lib/helpers.ts @@ -1,14 +1,16 @@ import { Headers as CrossFetchHeaders } from "cross-fetch"; -import { +import { Readable } from "stream"; +import merge from "deepmerge"; +import type { DeepgramClientOptions, FileSource, PrerecordedSource, UrlSource, TextSource, AnalyzeSource, + LiveSchema, + TranscriptionSchema, } from "./types"; -import { Readable } from "stream"; -import merge from "deepmerge"; export function stripTrailingSlash(url: string): string { return url.replace(/\/$/, ""); @@ -17,11 +19,8 @@ export function stripTrailingSlash(url: string): string { export const isBrowser = () => typeof window !== "undefined"; export const isServer = () => typeof process !== "undefined"; -export function applySettingDefaults( - options: DeepgramClientOptions, - defaults: DeepgramClientOptions -): DeepgramClientOptions { - return merge(defaults, options); +export function applyDefaults(options: Partial, subordinate: Partial): S { + return merge(subordinate, options); } export function appendSearchParams( @@ -83,5 +82,94 @@ const isReadStreamSource = (providedSource: PrerecordedSource): providedSource i }; export class CallbackUrl extends URL { - private callbackUrl = true; + public callbackUrl = true; +} + +export const convertProtocolToWs = (url: string) => { + const convert = (string: string) => string.toLowerCase().replace(/(http)(s)?/gi, "ws$2"); + + return convert(url); +}; + +export const buildRequestUrl = ( + endpoint: string, + baseUrl: string | URL, + transcriptionOptions: LiveSchema | TranscriptionSchema +): URL => { + const url = new URL(endpoint, baseUrl); + appendSearchParams(url.searchParams, transcriptionOptions); + + return url; +}; + +export function isLiveSchema(arg: any): arg is LiveSchema { + return arg && typeof arg.interim_results !== "undefined"; +} + +export function isDeepgramClientOptions(arg: any): arg is DeepgramClientOptions { + return arg && typeof arg.global !== "undefined"; } + +export const convertLegacyOptions = (optionsArg: DeepgramClientOptions): DeepgramClientOptions => { + const newOptions: DeepgramClientOptions = {}; + + if (optionsArg._experimentalCustomFetch) { + newOptions.global = { + fetch: { + client: optionsArg._experimentalCustomFetch, + }, + }; + } + + optionsArg = merge(optionsArg, newOptions); + + if (optionsArg.restProxy?.url) { + newOptions.global = { + fetch: { + options: { + proxy: { + url: optionsArg.restProxy?.url, + }, + }, + }, + }; + } + + optionsArg = merge(optionsArg, newOptions); + + if (optionsArg.global?.url) { + newOptions.global = { + fetch: { + options: { + url: optionsArg.global.url, + }, + }, + websocket: { + options: { + url: optionsArg.global.url, + }, + }, + }; + } + + optionsArg = merge(optionsArg, newOptions); + + if (optionsArg.global?.headers) { + newOptions.global = { + fetch: { + options: { + headers: optionsArg.global?.headers, + }, + }, + websocket: { + options: { + _nodeOnlyHeaders: optionsArg.global?.headers, + }, + }, + }; + } + + optionsArg = merge(optionsArg, newOptions); + + return optionsArg; +}; diff --git a/src/lib/types/CreateProjectKeySchema.ts b/src/lib/types/CreateProjectKeySchema.ts index 6ec1f65e..9f43b0b5 100644 --- a/src/lib/types/CreateProjectKeySchema.ts +++ b/src/lib/types/CreateProjectKeySchema.ts @@ -12,5 +12,4 @@ interface CommonOptions extends Record { comment: string; scopes: string[]; tags?: string[]; - [key: string]: unknown; } diff --git a/src/lib/types/DeepgramClientOptions.ts b/src/lib/types/DeepgramClientOptions.ts index 4b4d40ce..758add3a 100644 --- a/src/lib/types/DeepgramClientOptions.ts +++ b/src/lib/types/DeepgramClientOptions.ts @@ -1,19 +1,94 @@ import { Fetch, FetchOptions } from "./Fetch"; -export interface DeepgramClientOptions { - global?: { - /** - * Optional headers for initializing the client. - */ - headers?: Record; - - /** - * The URL used to interact with production, On-prem and other Deepgram environments. Defaults to `api.deepgram.com`. - */ - url?: string; +export type IKeyFactory = () => string; +export type IFetch = typeof fetch; +export type IWebSocket = typeof WebSocket; + +/** + * Defines the arguments for creating a Deepgram client. + * + * The `DeepgramClientArgs` type represents the possible arguments that can be passed when creating a Deepgram client. It can be either: + * + * 1. An array with two elements: + * - The first element is a string or an `IKeyFactory` object, representing the API key. + * - The second element is a `DeepgramClientOptions` object, representing the configuration options for the Deepgram client. + * 2. An array with a single `DeepgramClientOptions` object, representing the configuration options for the Deepgram client. + */ + +interface TransportFetchOptions extends TransportOptions, FetchOptions {} +interface TransportWebSocketOptions extends TransportOptions { + _nodeOnlyHeaders?: { [index: string]: any }; +} + +type TransportUrl = string; + +interface TransportOptions { + url?: TransportUrl; + proxy?: { + url: TransportUrl; + }; +} + +interface ITransport { + client?: C; + options?: O; +} + +export type DefaultNamespaceOptions = { + key: string; + fetch: { + options: { url: TransportUrl }; + }; + websocket: { + options: { url: TransportUrl }; }; +} & NamespaceOptions; + +export interface NamespaceOptions { + key?: string; + fetch?: ITransport; + websocket?: ITransport; +} + +export type DefaultClientOptions = { + global: Partial; +} & DeepgramClientOptions; + +/** + * Configures the options for a Deepgram client. + * + * The `DeepgramClientOptions` interface defines the configuration options for a Deepgram client. It includes options for various namespaces, such as `global`, `listen`, `manage`, `onprem`, `read`, and `speak`. Each namespace has its own options for configuring the transport, including the URL, proxy, and options for the fetch and WebSocket clients. + * + * The `global` namespace is used to configure options that apply globally to the Deepgram client. The other namespaces are used to configure options specific to different Deepgram API endpoints. + * Support introductory formats: + * - fetch: FetchOptions; + * - _experimentalCustomFetch?: Fetch; + * - restProxy?: { + * url: null | string; + * }; + */ +export interface DeepgramClientOptions { + key?: string | IKeyFactory; + global?: NamespaceOptions & { url?: string; headers?: { [index: string]: any } }; + listen?: NamespaceOptions; + manage?: NamespaceOptions; + onprem?: NamespaceOptions; + read?: NamespaceOptions; + speak?: NamespaceOptions; + + /** + * @deprecated as of 3.4, use a namespace like `global` instead + */ fetch?: FetchOptions; + + /** + * @deprecated as of 3.4, use a namespace like `global` instead + */ _experimentalCustomFetch?: Fetch; + + /** + * @deprecated as of 3.4, use a namespace like `global` instead + */ restProxy?: { url: null | string; }; diff --git a/src/lib/types/LiveConfigOptions.ts b/src/lib/types/LiveConfigOptions.ts index 6a12954b..47b60c87 100644 --- a/src/lib/types/LiveConfigOptions.ts +++ b/src/lib/types/LiveConfigOptions.ts @@ -1,3 +1,7 @@ -export interface LiveConfigOptions { - numerals?: boolean; -} +import { LiveSchema } from "./TranscriptionSchema"; + +/** + * Partial configuration options for the LiveSchema, including: + * - `numerals`: Configures how numerals are handled in the live transcription. + */ +export type LiveConfigOptions = Partial>; diff --git a/src/lib/types/TranscriptionSchema.ts b/src/lib/types/TranscriptionSchema.ts index 1ff2aaca..c35879e7 100644 --- a/src/lib/types/TranscriptionSchema.ts +++ b/src/lib/types/TranscriptionSchema.ts @@ -138,8 +138,6 @@ interface TranscriptionSchema extends Record { * @see https://developers.deepgram.com/docs/extra-metadata */ extra?: string[] | string; - - [key: string]: unknown; } interface PrerecordedSchema extends TranscriptionSchema { diff --git a/src/lib/types/UpdateProjectMemberScopeSchema.ts b/src/lib/types/UpdateProjectMemberScopeSchema.ts index 54193a48..f4a7e64a 100644 --- a/src/lib/types/UpdateProjectMemberScopeSchema.ts +++ b/src/lib/types/UpdateProjectMemberScopeSchema.ts @@ -1,3 +1,3 @@ -export interface UpdateProjectMemberScopeSchema { +export interface UpdateProjectMemberScopeSchema extends Record { scope: string; } diff --git a/src/lib/types/UpdateProjectSchema.ts b/src/lib/types/UpdateProjectSchema.ts index 329ea28f..f5b090e5 100644 --- a/src/lib/types/UpdateProjectSchema.ts +++ b/src/lib/types/UpdateProjectSchema.ts @@ -1,5 +1,4 @@ export interface UpdateProjectSchema extends Record { name?: string; company?: string; - [key: string]: unknown; } diff --git a/src/lib/types/index.ts b/src/lib/types/index.ts index 95ceb017..2f96bb17 100644 --- a/src/lib/types/index.ts +++ b/src/lib/types/index.ts @@ -1,54 +1,39 @@ -export type { AnalyzeSchema } from "./AnalyzeSchema"; -export type { AsyncPrerecordedResponse } from "./AsyncPrerecordedResponse"; -export type { AsyncAnalyzeResponse } from "./AsyncAnalyzeResponse"; -export type { CreateOnPremCredentialsSchema } from "./CreateOnPremCredentialsSchema"; -export type { CreateProjectKeySchema } from "./CreateProjectKeySchema"; -export type { CreateProjectKeyResponse } from "./CreateProjectKeyResponse"; -export type { DeepgramClientOptions } from "./DeepgramClientOptions"; -export type { DeepgramResponse } from "./DeepgramResponse"; -export type { Fetch } from "./Fetch"; -export type { - GetProjectBalanceResponse, - GetProjectBalancesResponse, -} from "./GetProjectBalancesResponse"; -export type { GetProjectInvitesResponse } from "./GetProjectInvitesResponse"; -export type { GetProjectKeyResponse, GetProjectKeysResponse } from "./GetProjectKeysResponse"; -export type { GetProjectMemberScopesResponse } from "./GetProjectMemberScopesResponse"; -export type { GetProjectMembersResponse } from "./GetProjectMembersResponse"; -export type { GetProjectResponse } from "./GetProjectResponse"; -export type { GetProjectsResponse } from "./GetProjectsResponse"; -export type { GetProjectUsageFieldsSchema } from "./GetProjectUsageFieldsSchema"; -export type { GetProjectUsageFieldsResponse } from "./GetProjectUsageFieldsResponse"; -export type { - GetProjectUsageRequestResponse, - GetProjectUsageRequestsResponse, -} from "./GetProjectUsageRequestsResponse"; -export type { GetProjectUsageRequestsSchema } from "./GetProjectUsageRequestsSchema"; -export type { GetProjectUsageSummarySchema } from "./GetProjectUsageSummarySchema"; -export type { GetProjectUsageSummaryResponse } from "./GetProjectUsageSummaryResponse"; -export type { GetTokenDetailsResponse } from "./GetTokenDetailsResponse"; -export type { - ListOnPremCredentialsResponse, - OnPremCredentialResponse, -} from "./ListOnPremCredentialsResponse"; -export type { LiveConfigOptions } from "./LiveConfigOptions"; -export type { LiveMetadataEvent } from "./LiveMetadataEvent"; -export type { LiveTranscriptionEvent } from "./LiveTranscriptionEvent"; -export type { MessageResponse } from "./MessageResponse"; -export type { - FileSource, - PrerecordedSource, - UrlSource, - TextSource, - AnalyzeSource, -} from "./DeepgramSource"; -export type { SendProjectInviteSchema } from "./SendProjectInviteSchema"; -export type { SpeakSchema } from "./SpeakSchema"; -export type { SpeechStartedEvent } from "./SpeechStartedEvent"; -export type { SyncPrerecordedResponse } from "./SyncPrerecordedResponse"; -export type { SyncAnalyzeResponse } from "./SyncAnalyzeResponse"; -export type { TranscriptionSchema, PrerecordedSchema, LiveSchema } from "./TranscriptionSchema"; -export type { UpdateProjectMemberScopeSchema } from "./UpdateProjectMemberScopeSchema"; -export type { UpdateProjectSchema } from "./UpdateProjectSchema"; -export type { UtteranceEndEvent } from "./UtteranceEndEvent"; -export type { VoidResponse } from "./VoidResponse"; +export type * from "./AnalyzeSchema"; +export type * from "./AsyncAnalyzeResponse"; +export type * from "./AsyncPrerecordedResponse"; +export type * from "./CreateOnPremCredentialsSchema"; +export type * from "./CreateProjectKeyResponse"; +export type * from "./CreateProjectKeySchema"; +export type * from "./DeepgramClientOptions"; +export type * from "./DeepgramResponse"; +export type * from "./DeepgramSource"; +export type * from "./Fetch"; +export type * from "./GetProjectBalancesResponse"; +export type * from "./GetProjectInvitesResponse"; +export type * from "./GetProjectKeysResponse"; +export type * from "./GetProjectMemberScopesResponse"; +export type * from "./GetProjectMembersResponse"; +export type * from "./GetProjectResponse"; +export type * from "./GetProjectsResponse"; +export type * from "./GetProjectUsageFieldsResponse"; +export type * from "./GetProjectUsageFieldsSchema"; +export type * from "./GetProjectUsageRequestsResponse"; +export type * from "./GetProjectUsageRequestsSchema"; +export type * from "./GetProjectUsageSummaryResponse"; +export type * from "./GetProjectUsageSummarySchema"; +export type * from "./GetTokenDetailsResponse"; +export type * from "./ListOnPremCredentialsResponse"; +export type * from "./LiveConfigOptions"; +export type * from "./LiveMetadataEvent"; +export type * from "./LiveTranscriptionEvent"; +export type * from "./MessageResponse"; +export type * from "./SendProjectInviteSchema"; +export type * from "./SpeakSchema"; +export type * from "./SpeechStartedEvent"; +export type * from "./SyncAnalyzeResponse"; +export type * from "./SyncPrerecordedResponse"; +export type * from "./TranscriptionSchema"; +export type * from "./UpdateProjectMemberScopeSchema"; +export type * from "./UpdateProjectSchema"; +export type * from "./UtteranceEndEvent"; +export type * from "./VoidResponse"; diff --git a/src/packages/AbstractClient.ts b/src/packages/AbstractClient.ts index 1ef95d49..9c8f3693 100644 --- a/src/packages/AbstractClient.ts +++ b/src/packages/AbstractClient.ts @@ -1,89 +1,144 @@ +import EventEmitter from "events"; import { DEFAULT_OPTIONS, DEFAULT_URL } from "../lib/constants"; import { DeepgramError } from "../lib/errors"; -import { applySettingDefaults, stripTrailingSlash } from "../lib/helpers"; -import { DeepgramClientOptions } from "../lib/types"; +import { appendSearchParams, applyDefaults, convertLegacyOptions } from "../lib/helpers"; +import type { + DeepgramClientOptions, + DefaultClientOptions, + DefaultNamespaceOptions, + NamespaceOptions, +} from "../lib/types"; + +export const noop = () => {}; /** - * Deepgram Client. + * Represents an abstract Deepgram client that provides a base implementation for interacting with the Deepgram API. + * + * The `AbstractClient` class is responsible for: + * - Initializing the Deepgram API key + * - Applying default options for the client and namespace + * - Providing a namespace for organizing API requests * - * An isomorphic Javascript client for interacting with the Deepgram API. - * @see https://developers.deepgram.com + * Subclasses of `AbstractClient` should implement the specific functionality for interacting with the Deepgram API. */ -export abstract class AbstractClient { - protected baseUrl: URL; - - constructor(protected key: string, protected options: DeepgramClientOptions) { - this.key = key; +export abstract class AbstractClient extends EventEmitter { + protected factory: Function | undefined = undefined; + protected key: string; + protected options: DefaultClientOptions; + public namespace: string = "global"; + public version: string = "v1"; + public baseUrl: string = DEFAULT_URL; + public logger: Function = noop; + + /** + * Constructs a new instance of the DeepgramClient class with the provided options. + * + * @param options - The options to configure the DeepgramClient instance. + * @param options.key - The Deepgram API key to use for authentication. If not provided, the `DEEPGRAM_API_KEY` environment variable will be used. + * @param options.global - Global options that apply to all requests made by the DeepgramClient instance. + * @param options.global.fetch - Options to configure the fetch requests made by the DeepgramClient instance. + * @param options.global.fetch.options - Additional options to pass to the fetch function, such as `url` and `headers`. + * @param options.namespace - Options specific to a particular namespace within the DeepgramClient instance. + */ + constructor(options: DeepgramClientOptions) { + super(); + + let key; + + if (typeof options.key === "function") { + this.factory = options.key; + key = this.factory(); + } else { + key = options.key; + } if (!key) { - this.key = process.env.DEEPGRAM_API_KEY as string; + key = process.env.DEEPGRAM_API_KEY as string; } - if (!this.key) { - throw new DeepgramError("A deepgram API key is required"); + if (!key) { + throw new DeepgramError("A deepgram API key is required."); } - this.options = applySettingDefaults(options, DEFAULT_OPTIONS); - - if (!this.options.global?.url) { - throw new DeepgramError( - `An API URL is required. It should be set to ${DEFAULT_URL} by default. No idea what happened!` - ); - } + this.key = key; - let baseUrlString: string = this.options.global.url; - let proxyUrlString: string; + options = convertLegacyOptions(options); /** - * Check if the base URL provided is missing a protocol and warn in the console. + * Apply default options. */ - if (!baseUrlString.startsWith("http") && !baseUrlString.startsWith("ws")) { - console.warn( - `The base URL provided does not begin with http, https, ws, or wss and will default to https as standard.` - ); + this.options = applyDefaults( + options, + DEFAULT_OPTIONS + ); + } + + public v(version: string = "v1"): this { + this.version = version; + + return this; + } + + get namespaceOptions(): DefaultNamespaceOptions { + const defaults = applyDefaults( + (this.options as any)[this.namespace], + this.options.global + ); + + return { + ...defaults, + key: this.key, + }; + } + + /** + * Generates a URL for an API endpoint with optional query parameters and transcription options. + * + * @param endpoint - The API endpoint URL, which may contain placeholders for fields. + * @param fields - An optional object containing key-value pairs to replace placeholders in the endpoint URL. + * @param transcriptionOptions - Optional transcription options to include as query parameters in the URL. + * @returns A URL object representing the constructed API request URL. + */ + public getRequestUrl( + endpoint: string, + fields: { [key: string]: string } = { version: this.version }, + transcriptionOptions?: { + [key: string]: unknown; } + ): URL { + /** + * If we pass in fields without a version, set a version. + */ + fields.version = this.version; /** - * Applying proxy to base URL. + * Version and template the endpoint for input argument.. */ - if (this.options.restProxy?.url) { - /** - * Prevent client using a real API key when using a proxy configuration. - */ - if (this.key !== "proxy") { - throw new DeepgramError( - `Do not attempt to pass any other API key than the string "proxy" when making proxied REST requests. Please ensure your proxy application is responsible for writing our API key to the Authorization header.` - ); - } - - proxyUrlString = this.options.restProxy.url; - - /** - * Check if the proxy URL provided is missing a protocol and warn in the console. - */ - if (!proxyUrlString.startsWith("http") && !proxyUrlString.startsWith("ws")) { - console.warn( - `The proxy URL provided does not begin with http, https, ws, or wss and will default to https as standard.` - ); - } - - baseUrlString = proxyUrlString; - } + endpoint = endpoint.replace(/:(\w+)/g, function (_, key) { + return fields![key]; + }); - this.baseUrl = this.resolveBaseUrl(baseUrlString); - } + /** + * Create a URL object. + */ + const url = new URL(endpoint as string, this.baseUrl); - protected resolveBaseUrl(url: string) { - if (!/^https?:\/\//i.test(url)) { - url = "https://" + url; + /** + * If there are transcription options, append them to the request as URL querystring parameters + */ + if (transcriptionOptions) { + appendSearchParams(url.searchParams, transcriptionOptions); } - return new URL(stripTrailingSlash(url)); + return url; } - protected willProxy() { - const proxyUrl = this.options.restProxy?.url; - - return !!proxyUrl; + /** + * Logs the message. + * + * For customized logging, `this.logger` can be overridden. + */ + public log(kind: string, msg: string, data?: any) { + this.logger(kind, msg, data); } } diff --git a/src/packages/AbstractLiveClient.ts b/src/packages/AbstractLiveClient.ts new file mode 100644 index 00000000..3e222f83 --- /dev/null +++ b/src/packages/AbstractLiveClient.ts @@ -0,0 +1,278 @@ +import { AbstractClient, noop } from "./AbstractClient"; +import { CONNECTION_STATE, SOCKET_STATES } from "../lib/constants"; +import type { DeepgramClientOptions, LiveSchema } from "../lib/types"; +import type { WebSocket as WSWebSocket } from "ws"; + +/** + * Represents a constructor for a WebSocket-like object that can be used in the application. + * The constructor takes the following parameters: + * @param address - The URL or address of the WebSocket server. + * @param _ignored - An optional parameter that is ignored. + * @param options - An optional object containing headers to be included in the WebSocket connection. + * @returns A WebSocket-like object that implements the WebSocketLike interface. + */ +interface WebSocketLikeConstructor { + new ( + address: string | URL, + _ignored?: any, + options?: { headers: Object | undefined } + ): WebSocketLike; +} + +/** + * Represents the types of WebSocket-like connections that can be used in the application. + * This type is used to provide a common interface for different WebSocket implementations, + * such as the native WebSocket API, a WebSocket wrapper library, or a dummy implementation + * for testing purposes. + */ +type WebSocketLike = WebSocket | WSWebSocket | WSWebSocketDummy; + +/** + * Represents the types of data that can be sent or received over a WebSocket-like connection. + */ +type SocketDataLike = string | ArrayBufferLike | Blob; + +/** + * Represents an error that occurred in a WebSocket-like connection. + * @property {any} error - The underlying error object. + * @property {string} message - A human-readable error message. + * @property {string} type - The type of the error. + */ +interface WebSocketLikeError { + error: any; + message: string; + type: string; +} + +/** + * Indicates whether a native WebSocket implementation is available in the current environment. + */ +const NATIVE_WEBSOCKET_AVAILABLE = typeof WebSocket !== "undefined"; + +/** + * Represents an abstract live client that extends the AbstractClient class. + * The AbstractLiveClient class provides functionality for connecting, reconnecting, and disconnecting a WebSocket connection, as well as sending data over the connection. + * Subclasses of this class are responsible for setting up the connection event handlers. + * + * @abstract + */ +export abstract class AbstractLiveClient extends AbstractClient { + public headers: { [key: string]: string }; + public transport: WebSocketLikeConstructor | null; + public conn: WebSocketLike | null = null; + public sendBuffer: Function[] = []; + + constructor(options: DeepgramClientOptions) { + super(options); + + const { + key, + websocket: { options: websocketOptions, client }, + } = this.namespaceOptions; + + if (this.proxy) { + this.baseUrl = websocketOptions.proxy!.url; + } else { + this.baseUrl = websocketOptions.url; + } + + if (client) { + this.transport = client; + } else { + this.transport = null; + } + + if (websocketOptions._nodeOnlyHeaders) { + this.headers = websocketOptions._nodeOnlyHeaders; + } else { + this.headers = {}; + } + + if (!("Authorization" in this.headers)) { + this.headers["Authorization"] = `Token ${key}`; // Add default token + } + } + + /** + * Connects the socket, unless already connected. + * + * @protected Can only be called from within the class. + */ + protected connect(transcriptionOptions: LiveSchema, endpoint: string): void { + if (this.conn) { + return; + } + + this.reconnect = (options = transcriptionOptions) => { + this.connect(options, endpoint); + }; + + const requestUrl = this.getRequestUrl(endpoint, {}, transcriptionOptions); + + /** + * Custom websocket transport + */ + if (this.transport) { + this.conn = new this.transport(requestUrl, undefined, { + headers: this.headers, + }); + return; + } + + /** + * Native websocket transport (browser) + */ + if (NATIVE_WEBSOCKET_AVAILABLE) { + this.conn = new WebSocket(requestUrl, ["token", this.namespaceOptions.key]); + this.setupConnection(); + return; + } + + /** + * Dummy websocket + */ + this.conn = new WSWebSocketDummy(requestUrl, undefined, { + close: () => { + this.conn = null; + }, + }); + + /** + * WS package for node environment + */ + import("ws").then(({ default: WS }) => { + this.conn = new WS(requestUrl, undefined, { + headers: this.headers, + }); + this.setupConnection(); + }); + } + + /** + * Reconnects the socket using new or existing transcription options. + * + * @param options - The transcription options to use when reconnecting the socket. + */ + public reconnect: (options: LiveSchema) => void = noop; + + /** + * Disconnects the socket from the client. + * + * @param code A numeric status code to send on disconnect. + * @param reason A custom reason for the disconnect. + */ + public disconnect(code?: number, reason?: string): void { + if (this.conn) { + this.conn.onclose = function () {}; // noop + if (code) { + this.conn.close(code, reason ?? ""); + } else { + this.conn.close(); + } + this.conn = null; + } + } + + /** + * Returns the current connection state of the WebSocket connection. + * + * @returns The current connection state of the WebSocket connection. + */ + public connectionState(): CONNECTION_STATE { + switch (this.conn && this.conn.readyState) { + case SOCKET_STATES.connecting: + return CONNECTION_STATE.Connecting; + case SOCKET_STATES.open: + return CONNECTION_STATE.Open; + case SOCKET_STATES.closing: + return CONNECTION_STATE.Closing; + default: + return CONNECTION_STATE.Closed; + } + } + + /** + * Returns the current ready state of the WebSocket connection. + * + * @returns The current ready state of the WebSocket connection. + */ + public getReadyState(): SOCKET_STATES { + return this.conn?.readyState ?? SOCKET_STATES.closed; + } + + /** + * Returns `true` is the connection is open. + */ + public isConnected(): boolean { + return this.connectionState() === CONNECTION_STATE.Open; + } + + /** + * Sends data to the Deepgram API via websocket connection + * @param data Audio data to send to Deepgram + * + * Conforms to RFC #146 for Node.js - does not send an empty byte. + * @see https://github.com/deepgram/deepgram-python-sdk/issues/146 + */ + send(data: SocketDataLike): void { + const callback = async () => { + if (data instanceof Blob) { + if (data.size === 0) { + this.log("warn", "skipping `send` for zero-byte blob", data); + + return; + } + + data = await data.arrayBuffer(); + } + + if (typeof data !== "string") { + if (data.byteLength === 0) { + this.log("warn", "skipping `send` for zero-byte blob", data); + + return; + } + } + + this.conn?.send(data); + }; + + if (this.isConnected()) { + callback(); + } else { + this.sendBuffer.push(callback); + } + } + + /** + * Determines whether the current instance should proxy requests. + * @returns {boolean} true if the current instance should proxy requests; otherwise, false + */ + get proxy(): boolean { + return this.key === "proxy" && !!this.namespaceOptions.websocket.options.proxy?.url; + } + + /** + * Sets up the connection event handlers. + * + * @abstract Requires subclasses to set up context aware event handlers. + */ + abstract setupConnection(): void; +} + +class WSWebSocketDummy { + binaryType: string = "arraybuffer"; + close: Function; + onclose: Function = () => {}; + onerror: Function = () => {}; + onmessage: Function = () => {}; + onopen: Function = () => {}; + readyState: number = SOCKET_STATES.connecting; + send: Function = () => {}; + url: string | URL | null = null; + + constructor(address: URL, _protocols: undefined, options: { close: Function }) { + this.url = address.toString(); + this.close = options.close; + } +} diff --git a/src/packages/AbstractRestfulClient.ts b/src/packages/AbstractRestClient.ts similarity index 76% rename from src/packages/AbstractRestfulClient.ts rename to src/packages/AbstractRestClient.ts index 1dd36cb7..2efaf283 100644 --- a/src/packages/AbstractRestfulClient.ts +++ b/src/packages/AbstractRestClient.ts @@ -1,31 +1,42 @@ +import { AbstractClient } from "./AbstractClient"; import { DeepgramApiError, DeepgramError, DeepgramUnknownError } from "../lib/errors"; -import { Readable } from "stream"; import { fetchWithAuth, resolveResponse } from "../lib/fetch"; -import type { Fetch, FetchParameters, RequestMethodType } from "../lib/types/Fetch"; -import { AbstractClient } from "./AbstractClient"; -import { DeepgramClientOptions } from "../lib/types"; import { isBrowser } from "../lib/helpers"; - -export abstract class AbstractRestfulClient extends AbstractClient { +import { Readable } from "stream"; +import type { + DeepgramClientOptions, + Fetch, + FetchParameters, + RequestMethodType, +} from "../lib/types"; + +export abstract class AbstractRestClient extends AbstractClient { protected fetch: Fetch; - constructor(protected key: string, protected options: DeepgramClientOptions) { - super(key, options); + // Constructor implementation + constructor(options: DeepgramClientOptions) { + super(options); - if (isBrowser() && !this._willProxy()) { + if (isBrowser() && !this.proxy) { throw new DeepgramError( - "Due to CORS we are unable to support REST-based API calls to our API from the browser. Please consider using a proxy, and including a `restProxy: { url: ''}` in your Deepgram client options." + "Due to CORS we are unable to support REST-based API calls to our API from the browser. Please consider using a proxy: https://dpgr.am/js-proxy for more information." ); } - this.fetch = fetchWithAuth(this.key, options._experimentalCustomFetch); + this.fetch = fetchWithAuth(this.namespaceOptions.key, this.namespaceOptions.fetch.client); + + if (this.proxy) { + this.baseUrl = this.namespaceOptions.fetch.options.proxy!.url; + } else { + this.baseUrl = this.namespaceOptions.fetch.options.url; + } } protected _getErrorMessage(err: any): string { return err.msg || err.message || err.error_description || err.error || JSON.stringify(err); } - protected async handleError(error: unknown, reject: (reason?: any) => void) { + protected async _handleError(error: unknown, reject: (reason?: any) => void) { const Res = await resolveResponse(); if (error instanceof Res) { @@ -80,7 +91,7 @@ export abstract class AbstractRestfulClient extends AbstractClient { return result.json(); }) .then((data) => resolve(data)) - .catch((error) => this.handleError(error, reject)); + .catch((error) => this._handleError(error, reject)); }); } @@ -100,7 +111,7 @@ export abstract class AbstractRestfulClient extends AbstractClient { return result; }) .then((data) => resolve(data)) - .catch((error) => this.handleError(error, reject)); + .catch((error) => this._handleError(error, reject)); }); } @@ -152,9 +163,11 @@ export abstract class AbstractRestfulClient extends AbstractClient { return this._handleRequest(fetcher, "DELETE", url, headers, parameters); } - private _willProxy() { - const proxyUrl = this.options.restProxy?.url; - - return !!proxyUrl; + /** + * Determines whether the current instance should proxy requests. + * @returns {boolean} true if the current instance should proxy requests; otherwise, false + */ + get proxy(): boolean { + return this.key === "proxy" && !!this.namespaceOptions.fetch.options.proxy?.url; } } diff --git a/src/packages/AbstractWsClient.ts b/src/packages/AbstractWsClient.ts deleted file mode 100644 index 81a8b608..00000000 --- a/src/packages/AbstractWsClient.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { EventEmitter } from "events"; -import { DEFAULT_OPTIONS, DEFAULT_URL } from "../lib/constants"; -import { applySettingDefaults, stripTrailingSlash } from "../lib/helpers"; -import { DeepgramClientOptions } from "../lib/types"; - -export abstract class AbstractWsClient extends EventEmitter { - protected baseUrl: URL; - - constructor( - protected key: string, - protected options: DeepgramClientOptions | undefined = DEFAULT_OPTIONS - ) { - super(); - - this.key = key; - - if (!key) { - this.key = process.env.DEEPGRAM_API_KEY as string; - } - - if (!this.key) { - throw new Error("A deepgram API key is required"); - } - - this.options = applySettingDefaults(options, DEFAULT_OPTIONS); - - if (!this.options.global?.url) { - throw new Error( - `An API URL is required. It should be set to ${DEFAULT_URL} by default. No idea what happened!` - ); - } - - let url = this.options.global.url; - - if (!/^https?:\/\//i.test(url)) { - url = "https://" + url; - } - - this.baseUrl = new URL(stripTrailingSlash(url)); - this.baseUrl.protocol = this.baseUrl.protocol.toLowerCase().replace(/(http)(s)?/gi, "ws$2"); - } -} diff --git a/src/packages/ListenClient.ts b/src/packages/ListenClient.ts index 5d6d823b..58fe854a 100644 --- a/src/packages/ListenClient.ts +++ b/src/packages/ListenClient.ts @@ -1,14 +1,16 @@ import { AbstractClient } from "./AbstractClient"; -import { LiveClient } from "./LiveClient"; +import { ListenLiveClient } from "./ListenLiveClient"; import { LiveSchema } from "../lib/types"; -import { PrerecordedClient } from "./PrerecordedClient"; +import { ListenRestClient } from "./ListenRestClient"; export class ListenClient extends AbstractClient { + public namespace: string = "listen"; + get prerecorded() { - return new PrerecordedClient(this.key, this.options); + return new ListenRestClient(this.options); } - public live(transcriptionOptions: LiveSchema, endpoint = "v1/listen") { - return new LiveClient(this.key, this.options, transcriptionOptions, endpoint); + public live(transcriptionOptions: LiveSchema = {}, endpoint = ":version/listen") { + return new ListenLiveClient(this.options, transcriptionOptions, endpoint); } } diff --git a/src/packages/ListenLiveClient.ts b/src/packages/ListenLiveClient.ts new file mode 100644 index 00000000..4b38e22a --- /dev/null +++ b/src/packages/ListenLiveClient.ts @@ -0,0 +1,110 @@ +import { AbstractLiveClient } from "./AbstractLiveClient"; +import { LiveTranscriptionEvents } from "../lib/enums"; +import type { LiveSchema, LiveConfigOptions, DeepgramClientOptions } from "../lib/types"; + +export class ListenLiveClient extends AbstractLiveClient { + public namespace: string = "listen"; + + // Constructor implementation + constructor( + options: DeepgramClientOptions, + transcriptionOptions: LiveSchema = {}, + endpoint: string = ":version/listen" + ) { + super(options); + + this.connect(transcriptionOptions, endpoint); + } + + /** + * Sets up the connection event handlers. + * This method is responsible for handling the various events that can occur on the WebSocket connection, such as opening, closing, and receiving messages. + * - When the connection is opened, it emits the `LiveTranscriptionEvents.Open` event. + * - When the connection is closed, it emits the `LiveTranscriptionEvents.Close` event. + * - When an error occurs on the connection, it emits the `LiveTranscriptionEvents.Error` event. + * - When a message is received, it parses the message and emits the appropriate event based on the message type, such as `LiveTranscriptionEvents.Metadata`, `LiveTranscriptionEvents.Transcript`, `LiveTranscriptionEvents.UtteranceEnd`, and `LiveTranscriptionEvents.SpeechStarted`. + */ + public setupConnection(): void { + if (this.conn) { + this.conn.onopen = () => { + this.emit(LiveTranscriptionEvents.Open, this); + }; + + this.conn.onclose = (event: any) => { + this.emit(LiveTranscriptionEvents.Close, event); + }; + + this.conn.onerror = (event: ErrorEvent) => { + this.emit(LiveTranscriptionEvents.Error, event); + }; + + this.conn.onmessage = (event: MessageEvent) => { + try { + const data: any = JSON.parse(event.data.toString()); + + if (data.type === LiveTranscriptionEvents.Metadata) { + this.emit(LiveTranscriptionEvents.Metadata, data); + } else if (data.type === LiveTranscriptionEvents.Transcript) { + this.emit(LiveTranscriptionEvents.Transcript, data); + } else if (data.type === LiveTranscriptionEvents.UtteranceEnd) { + this.emit(LiveTranscriptionEvents.UtteranceEnd, data); + } else if (data.type === LiveTranscriptionEvents.SpeechStarted) { + this.emit(LiveTranscriptionEvents.SpeechStarted, data); + } else { + this.emit(LiveTranscriptionEvents.Unhandled, data); + } + } catch (error) { + this.emit(LiveTranscriptionEvents.Error, { + event, + message: "Unable to parse `data` as JSON.", + error, + }); + } + }; + } + } + + /** + * Sends additional config to the connected session. + * + * @param config - The configuration options to apply to the LiveClient. + * @param config.numerals - We currently only support numerals. + */ + public configure(config: LiveConfigOptions): void { + this.send( + JSON.stringify({ + type: "Configure", + processors: config, + }) + ); + } + + /** + * Sends a "KeepAlive" message to the server to maintain the connection. + */ + public keepAlive(): void { + this.send( + JSON.stringify({ + type: "KeepAlive", + }) + ); + } + + /** + * @deprecated Since version 3.4. Will be removed in version 4.0. Use `close` instead. + */ + public finish(): void { + this.requestClose(); + } + + /** + * Requests the server close the connection. + */ + public requestClose(): void { + this.send( + JSON.stringify({ + type: "CloseStream", + }) + ); + } +} diff --git a/src/packages/PrerecordedClient.ts b/src/packages/ListenRestClient.ts similarity index 61% rename from src/packages/PrerecordedClient.ts rename to src/packages/ListenRestClient.ts index 3575fa99..ba1b3445 100644 --- a/src/packages/PrerecordedClient.ts +++ b/src/packages/ListenRestClient.ts @@ -1,5 +1,5 @@ -import { AbstractRestfulClient } from "./AbstractRestfulClient"; -import { CallbackUrl, appendSearchParams, isFileSource, isUrlSource } from "../lib/helpers"; +import { AbstractRestClient } from "./AbstractRestClient"; +import { CallbackUrl, isFileSource, isUrlSource } from "../lib/helpers"; import { DeepgramError, isDeepgramError } from "../lib/errors"; import type { AsyncPrerecordedResponse, @@ -11,11 +11,13 @@ import type { UrlSource, } from "../lib/types"; -export class PrerecordedClient extends AbstractRestfulClient { +export class ListenRestClient extends AbstractRestClient { + public namespace: string = "listen"; + async transcribeUrl( source: UrlSource, options?: PrerecordedSchema, - endpoint = "v1/listen" + endpoint = ":version/listen" ): Promise> { try { let body; @@ -32,12 +34,12 @@ export class PrerecordedClient extends AbstractRestfulClient { ); } - const transcriptionOptions: PrerecordedSchema = { ...{}, ...options }; - - const url = new URL(endpoint, this.baseUrl); - appendSearchParams(url.searchParams, transcriptionOptions); - - const result: SyncPrerecordedResponse = await this.post(this.fetch as Fetch, url, body); + const requestUrl = this.getRequestUrl(endpoint, {}, { ...{}, ...options }); + const result: SyncPrerecordedResponse = await this.post( + this.fetch as Fetch, + requestUrl, + body + ); return { result, error: null }; } catch (error) { @@ -52,7 +54,7 @@ export class PrerecordedClient extends AbstractRestfulClient { async transcribeFile( source: FileSource, options?: PrerecordedSchema, - endpoint = "v1/listen" + endpoint = ":version/listen" ): Promise> { try { let body; @@ -69,14 +71,15 @@ export class PrerecordedClient extends AbstractRestfulClient { ); } - const transcriptionOptions: PrerecordedSchema = { ...{}, ...options }; - - const url = new URL(endpoint, this.baseUrl); - appendSearchParams(url.searchParams, transcriptionOptions); - - const result: SyncPrerecordedResponse = await this.post(this.fetch as Fetch, url, body, { - "Content-Type": "deepgram/audio+video", - }); + const requestUrl = this.getRequestUrl(endpoint, {}, { ...{}, ...options }); + const result: SyncPrerecordedResponse = await this.post( + this.fetch as Fetch, + requestUrl, + body, + { + "Content-Type": "deepgram/audio+video", + } + ); return { result, error: null }; } catch (error) { @@ -92,7 +95,7 @@ export class PrerecordedClient extends AbstractRestfulClient { source: UrlSource, callback: CallbackUrl, options?: PrerecordedSchema, - endpoint = "v1/listen" + endpoint = ":version/listen" ): Promise> { try { let body; @@ -103,15 +106,16 @@ export class PrerecordedClient extends AbstractRestfulClient { throw new DeepgramError("Unknown transcription source type"); } - const transcriptionOptions: PrerecordedSchema = { - ...options, - ...{ callback: callback.toString() }, - }; - - const url = new URL(endpoint, this.baseUrl); - appendSearchParams(url.searchParams, transcriptionOptions); - - const result: AsyncPrerecordedResponse = await this.post(this.fetch as Fetch, url, body); + const requestUrl = this.getRequestUrl( + endpoint, + {}, + { ...options, callback: callback.toString() } + ); + const result: AsyncPrerecordedResponse = await this.post( + this.fetch as Fetch, + requestUrl, + body + ); return { result, error: null }; } catch (error) { @@ -127,7 +131,7 @@ export class PrerecordedClient extends AbstractRestfulClient { source: FileSource, callback: CallbackUrl, options?: PrerecordedSchema, - endpoint = "v1/listen" + endpoint = ":version/listen" ): Promise> { try { let body; @@ -138,17 +142,27 @@ export class PrerecordedClient extends AbstractRestfulClient { throw new DeepgramError("Unknown transcription source type"); } - const transcriptionOptions: PrerecordedSchema = { - ...options, - ...{ callback: callback.toString() }, - }; - - const url = new URL(endpoint, this.baseUrl); - appendSearchParams(url.searchParams, transcriptionOptions); - - const result: AsyncPrerecordedResponse = await this.post(this.fetch as Fetch, url, body, { - "Content-Type": "deepgram/audio+video", - }); + // const transcriptionOptions: PrerecordedSchema = { + // ...options, + // ...{ callback: callback.toString() }, + // }; + + // const url = new URL(endpoint, this.baseUrl); + // appendSearchParams(url.searchParams, transcriptionOptions); + + const requestUrl = this.getRequestUrl( + endpoint, + {}, + { ...options, callback: callback.toString() } + ); + const result: AsyncPrerecordedResponse = await this.post( + this.fetch as Fetch, + requestUrl, + body, + { + "Content-Type": "deepgram/audio+video", + } + ); return { result, error: null }; } catch (error) { diff --git a/src/packages/LiveClient.ts b/src/packages/LiveClient.ts deleted file mode 100644 index d1353cb1..00000000 --- a/src/packages/LiveClient.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { AbstractWsClient } from "./AbstractWsClient"; -import { appendSearchParams } from "../lib/helpers"; -import { DeepgramError } from "../lib/errors"; -import { DEFAULT_OPTIONS } from "../lib/constants"; -import { LiveConnectionState, LiveTranscriptionEvents } from "../lib/enums"; -import { w3cwebsocket } from "websocket"; - -import type { - LiveSchema, - LiveConfigOptions, - LiveMetadataEvent, - LiveTranscriptionEvent, - DeepgramClientOptions, - UtteranceEndEvent, - SpeechStartedEvent, -} from "../lib/types"; - -export class LiveClient extends AbstractWsClient { - private _socket: w3cwebsocket; - - constructor( - protected key: string, - protected options: DeepgramClientOptions | undefined = DEFAULT_OPTIONS, - private transcriptionOptions: LiveSchema = {}, - endpoint = "v1/listen" - ) { - super(key, options); - - const url = new URL(endpoint, this.baseUrl); - url.protocol = url.protocol.toLowerCase().replace(/(http)(s)?/gi, "ws$2"); - appendSearchParams(url.searchParams, this.transcriptionOptions); - - this._socket = new w3cwebsocket(url.toString(), ["token", this.key]); - - this._socket.onopen = () => { - this.emit(LiveTranscriptionEvents.Open, this); - }; - - this._socket.onclose = (event: any) => { - this.emit(LiveTranscriptionEvents.Close, event); - }; - - this._socket.onerror = (event) => { - this.emit(LiveTranscriptionEvents.Error, event); - }; - - this._socket.onmessage = (event) => { - try { - const data: any = JSON.parse(event.data.toString()); - - if (data.type === LiveTranscriptionEvents.Metadata) { - this.emit(LiveTranscriptionEvents.Metadata, data as LiveMetadataEvent); - } - - if (data.type === LiveTranscriptionEvents.Transcript) { - this.emit(LiveTranscriptionEvents.Transcript, data as LiveTranscriptionEvent); - } - - if (data.type === LiveTranscriptionEvents.UtteranceEnd) { - this.emit(LiveTranscriptionEvents.UtteranceEnd, data as UtteranceEndEvent); - } - - if (data.type === LiveTranscriptionEvents.SpeechStarted) { - this.emit(LiveTranscriptionEvents.SpeechStarted, data as SpeechStartedEvent); - } - } catch (error) { - this.emit(LiveTranscriptionEvents.Error, { - event, - message: "Unable to parse `data` as JSON.", - error, - }); - } - }; - } - - public configure(config: LiveConfigOptions): void { - this._socket.send( - JSON.stringify({ - type: "Configure", - processors: config, - }) - ); - } - - public keepAlive(): void { - this._socket.send( - JSON.stringify({ - type: "KeepAlive", - }) - ); - } - - public getReadyState(): LiveConnectionState { - return this._socket.readyState; - } - - /** - * Sends data to the Deepgram API via websocket connection - * @param data Audio data to send to Deepgram - * - * Conforms to RFC #146 for Node.js - does not send an empty byte. - * In the browser, a Blob will contain length with no audio. - * @see https://github.com/deepgram/deepgram-python-sdk/issues/146 - */ - public send(data: string | ArrayBufferLike | Blob): void { - if (this._socket.readyState === LiveConnectionState.OPEN) { - if (typeof data === "string") { - this._socket.send(data); // send text data - } else if ((data as any) instanceof Blob) { - this._socket.send(data as unknown as ArrayBufferLike); // send blob data - } else { - const buffer = data as ArrayBufferLike; - - if (buffer.byteLength > 0) { - this._socket.send(buffer); // send buffer when not zero-byte (or browser) - } else { - this.emit( - LiveTranscriptionEvents.Warning, - "Zero-byte detected, skipping. Send `CloseStream` if trying to close the connection." - ); - } - } - } else { - throw new DeepgramError("Could not send. Connection not open."); - } - } - - /** - * Denote that you are finished sending audio and close - * the websocket connection when transcription is finished - */ - public finish(): void { - // tell the server to close the socket - this._socket.send( - JSON.stringify({ - type: "CloseStream", - }) - ); - } -} diff --git a/src/packages/ManageClient.ts b/src/packages/ManageClient.ts index 5278a416..9f6b49da 100644 --- a/src/packages/ManageClient.ts +++ b/src/packages/ManageClient.ts @@ -1,9 +1,8 @@ -import { AbstractRestfulClient } from "./AbstractRestfulClient"; +import { AbstractRestClient } from "./AbstractRestClient"; import { isDeepgramError } from "../lib/errors"; -import { appendSearchParams } from "../lib/helpers"; import type { - CreateProjectKeySchema, CreateProjectKeyResponse, + CreateProjectKeySchema, DeepgramResponse, Fetch, GetProjectBalanceResponse, @@ -15,33 +14,33 @@ import type { GetProjectMembersResponse, GetProjectResponse, GetProjectsResponse, - GetProjectUsageFieldsSchema, GetProjectUsageFieldsResponse, + GetProjectUsageFieldsSchema, GetProjectUsageRequestResponse, - GetProjectUsageRequestsSchema, GetProjectUsageRequestsResponse, - GetProjectUsageSummarySchema, + GetProjectUsageRequestsSchema, GetProjectUsageSummaryResponse, + GetProjectUsageSummarySchema, + GetTokenDetailsResponse, MessageResponse, SendProjectInviteSchema, UpdateProjectMemberScopeSchema, UpdateProjectSchema, VoidResponse, - GetTokenDetailsResponse, } from "../lib/types"; -export class ManageClient extends AbstractRestfulClient { +export class ManageClient extends AbstractRestClient { + public namespace: string = "manage"; + /** * @see https://developers.deepgram.com/docs/authenticating#test-request */ async getTokenDetails( - endpoint = "v1/auth/token" + endpoint = ":version/auth/token" ): Promise> { try { - const url = new URL(this.baseUrl); - url.pathname = endpoint; - - const result: GetTokenDetailsResponse = await this.get(this.fetch as Fetch, url); + const requestUrl = this.getRequestUrl(endpoint); + const result: GetTokenDetailsResponse = await this.get(this.fetch as Fetch, requestUrl); return { result, error: null }; } catch (error) { @@ -56,12 +55,12 @@ export class ManageClient extends AbstractRestfulClient { /** * @see https://developers.deepgram.com/reference/get-projects */ - async getProjects(endpoint = "v1/projects"): Promise> { + async getProjects( + endpoint = ":version/projects" + ): Promise> { try { - const url = new URL(this.baseUrl); - url.pathname = endpoint; - - const result: GetProjectsResponse = await this.get(this.fetch as Fetch, url); + const requestUrl = this.getRequestUrl(endpoint); + const result: GetProjectsResponse = await this.get(this.fetch as Fetch, requestUrl); return { result, error: null }; } catch (error) { @@ -78,13 +77,11 @@ export class ManageClient extends AbstractRestfulClient { */ async getProject( projectId: string, - endpoint = "v1/projects/:projectId" + endpoint = ":version/projects/:projectId" ): Promise> { try { - const url = new URL(this.baseUrl); - url.pathname = endpoint.replace(/:projectId/, projectId); - - const result: GetProjectResponse = await this.get(this.fetch as Fetch, url); + const requestUrl = this.getRequestUrl(endpoint, { projectId }); + const result: GetProjectResponse = await this.get(this.fetch as Fetch, requestUrl); return { result, error: null }; } catch (error) { @@ -102,15 +99,13 @@ export class ManageClient extends AbstractRestfulClient { async updateProject( projectId: string, options: UpdateProjectSchema, - endpoint = "v1/projects/:projectId" + endpoint = ":version/projects/:projectId" ): Promise> { try { - const url = new URL(this.baseUrl); - url.pathname = endpoint.replace(/:projectId/, projectId); - + const requestUrl = this.getRequestUrl(endpoint, { projectId }, options); const body = JSON.stringify(options); - const result: MessageResponse = await this.patch(this.fetch as Fetch, url, body); + const result: MessageResponse = await this.patch(this.fetch as Fetch, requestUrl, body); return { result, error: null }; } catch (error) { @@ -127,13 +122,11 @@ export class ManageClient extends AbstractRestfulClient { */ async deleteProject( projectId: string, - endpoint = "v1/projects/:projectId" + endpoint = ":version/projects/:projectId" ): Promise { try { - const url = new URL(this.baseUrl); - url.pathname = endpoint.replace(/:projectId/, projectId); - - await this.delete(this.fetch as Fetch, url); + const requestUrl = this.getRequestUrl(endpoint, { projectId }); + await this.delete(this.fetch as Fetch, requestUrl); return { error: null }; } catch (error) { @@ -150,13 +143,11 @@ export class ManageClient extends AbstractRestfulClient { */ async getProjectKeys( projectId: string, - endpoint = "v1/projects/:projectId/keys" + endpoint = ":version/projects/:projectId/keys" ): Promise> { try { - const url = new URL(this.baseUrl); - url.pathname = endpoint.replace(/:projectId/, projectId); - - const result: GetProjectKeysResponse = await this.get(this.fetch as Fetch, url); + const requestUrl = this.getRequestUrl(endpoint, { projectId }); + const result: GetProjectKeysResponse = await this.get(this.fetch as Fetch, requestUrl); return { result, error: null }; } catch (error) { @@ -174,13 +165,11 @@ export class ManageClient extends AbstractRestfulClient { async getProjectKey( projectId: string, keyId: string, - endpoint = "v1/projects/:projectId/keys/:keyId" + endpoint = ":version/projects/:projectId/keys/:keyId" ): Promise> { try { - const url = new URL(this.baseUrl); - url.pathname = endpoint.replace(/:projectId/, projectId).replace(/:keyId/, keyId); - - const result: GetProjectKeyResponse = await this.get(this.fetch as Fetch, url); + const requestUrl = this.getRequestUrl(endpoint, { projectId, keyId }); + const result: GetProjectKeyResponse = await this.get(this.fetch as Fetch, requestUrl); return { result, error: null }; } catch (error) { @@ -198,15 +187,17 @@ export class ManageClient extends AbstractRestfulClient { async createProjectKey( projectId: string, options: CreateProjectKeySchema, - endpoint = "v1/projects/:projectId/keys" + endpoint = ":version/projects/:projectId/keys" ): Promise> { try { - const url = new URL(this.baseUrl); - url.pathname = endpoint.replace(/:projectId/, projectId); - + const requestUrl = this.getRequestUrl(endpoint, { projectId }, options); const body = JSON.stringify(options); - const result: CreateProjectKeyResponse = await this.post(this.fetch as Fetch, url, body); + const result: CreateProjectKeyResponse = await this.post( + this.fetch as Fetch, + requestUrl, + body + ); return { result, error: null }; } catch (error) { @@ -224,13 +215,11 @@ export class ManageClient extends AbstractRestfulClient { async deleteProjectKey( projectId: string, keyId: string, - endpoint = "v1/projects/:projectId/keys/:keyId" + endpoint = ":version/projects/:projectId/keys/:keyId" ): Promise { try { - const url = new URL(this.baseUrl); - url.pathname = endpoint.replace(/:projectId/, projectId).replace(/:keyId/, keyId); - - await this.delete(this.fetch as Fetch, url); + const requestUrl = this.getRequestUrl(endpoint, { projectId, keyId }); + await this.delete(this.fetch as Fetch, requestUrl); return { error: null }; } catch (error) { @@ -247,13 +236,11 @@ export class ManageClient extends AbstractRestfulClient { */ async getProjectMembers( projectId: string, - endpoint = "v1/projects/:projectId/members" + endpoint = ":version/projects/:projectId/members" ): Promise> { try { - const url = new URL(this.baseUrl); - url.pathname = endpoint.replace(/:projectId/, projectId); - - const result: GetProjectMembersResponse = await this.get(this.fetch as Fetch, url); + const requestUrl = this.getRequestUrl(endpoint, { projectId }); + const result: GetProjectMembersResponse = await this.get(this.fetch as Fetch, requestUrl); return { result, error: null }; } catch (error) { @@ -271,13 +258,11 @@ export class ManageClient extends AbstractRestfulClient { async removeProjectMember( projectId: string, memberId: string, - endpoint = "v1/projects/:projectId/members/:memberId" + endpoint = ":version/projects/:projectId/members/:memberId" ): Promise { try { - const url = new URL(this.baseUrl); - url.pathname = endpoint.replace(/:projectId/, projectId).replace(/:memberId/, memberId); - - await this.delete(this.fetch as Fetch, url); + const requestUrl = this.getRequestUrl(endpoint, { projectId, memberId }); + await this.delete(this.fetch as Fetch, requestUrl); return { error: null }; } catch (error) { @@ -295,13 +280,14 @@ export class ManageClient extends AbstractRestfulClient { async getProjectMemberScopes( projectId: string, memberId: string, - endpoint = "v1/projects/:projectId/members/:memberId/scopes" + endpoint = ":version/projects/:projectId/members/:memberId/scopes" ): Promise> { try { - const url = new URL(this.baseUrl); - url.pathname = endpoint.replace(/:projectId/, projectId).replace(/:memberId/, memberId); - - const result: GetProjectMemberScopesResponse = await this.get(this.fetch as Fetch, url); + const requestUrl = this.getRequestUrl(endpoint, { projectId, memberId }); + const result: GetProjectMemberScopesResponse = await this.get( + this.fetch as Fetch, + requestUrl + ); return { result, error: null }; } catch (error) { @@ -320,15 +306,13 @@ export class ManageClient extends AbstractRestfulClient { projectId: string, memberId: string, options: UpdateProjectMemberScopeSchema, - endpoint = "v1/projects/:projectId/members/:memberId/scopes" + endpoint = ":version/projects/:projectId/members/:memberId/scopes" ): Promise> { try { - const url = new URL(this.baseUrl); - url.pathname = endpoint.replace(/:projectId/, projectId).replace(/:memberId/, memberId); - + const requestUrl = this.getRequestUrl(endpoint, { projectId, memberId }, options); const body = JSON.stringify(options); - const result: MessageResponse = await this.put(this.fetch as Fetch, url, body); + const result: MessageResponse = await this.put(this.fetch as Fetch, requestUrl, body); return { result, error: null }; } catch (error) { @@ -345,13 +329,11 @@ export class ManageClient extends AbstractRestfulClient { */ async getProjectInvites( projectId: string, - endpoint = "v1/projects/:projectId/invites" + endpoint = ":version/projects/:projectId/invites" ): Promise> { try { - const url = new URL(this.baseUrl); - url.pathname = endpoint.replace(/:projectId/, projectId); - - const result: GetProjectInvitesResponse = await this.get(this.fetch as Fetch, url); + const requestUrl = this.getRequestUrl(endpoint, { projectId }); + const result: GetProjectInvitesResponse = await this.get(this.fetch as Fetch, requestUrl); return { result, error: null }; } catch (error) { @@ -369,15 +351,13 @@ export class ManageClient extends AbstractRestfulClient { async sendProjectInvite( projectId: string, options: SendProjectInviteSchema, - endpoint = "v1/projects/:projectId/invites" + endpoint = ":version/projects/:projectId/invites" ): Promise> { try { - const url = new URL(this.baseUrl); - url.pathname = endpoint.replace(/:projectId/, projectId); - + const requestUrl = this.getRequestUrl(endpoint, { projectId }, options); const body = JSON.stringify(options); - const result: MessageResponse = await this.post(this.fetch as Fetch, url, body); + const result: MessageResponse = await this.post(this.fetch as Fetch, requestUrl, body); return { result, error: null }; } catch (error) { @@ -395,13 +375,11 @@ export class ManageClient extends AbstractRestfulClient { async deleteProjectInvite( projectId: string, email: string, - endpoint = "v1/projects/:projectId/invites/:email" + endpoint = ":version/projects/:projectId/invites/:email" ): Promise { try { - const url = new URL(this.baseUrl); - url.pathname = endpoint.replace(/:projectId/, projectId).replace(/:email/, email); - - await this.delete(this.fetch as Fetch, url); + const requestUrl = this.getRequestUrl(endpoint, { projectId, email }); + await this.delete(this.fetch as Fetch, requestUrl); return { error: null }; } catch (error) { @@ -418,13 +396,11 @@ export class ManageClient extends AbstractRestfulClient { */ async leaveProject( projectId: string, - endpoint = "v1/projects/:projectId/leave" + endpoint = ":version/projects/:projectId/leave" ): Promise> { try { - const url = new URL(this.baseUrl); - url.pathname = endpoint.replace(/:projectId/, projectId); - - const result: MessageResponse = await this.delete(this.fetch as Fetch, url); + const requestUrl = this.getRequestUrl(endpoint, { projectId }); + const result: MessageResponse = await this.delete(this.fetch as Fetch, requestUrl); return { result, error: null }; } catch (error) { @@ -442,14 +418,14 @@ export class ManageClient extends AbstractRestfulClient { async getProjectUsageRequests( projectId: string, options: GetProjectUsageRequestsSchema, - endpoint = "v1/projects/:projectId/requests" + endpoint = ":version/projects/:projectId/requests" ): Promise> { try { - const url = new URL(this.baseUrl); - url.pathname = endpoint.replace(/:projectId/, projectId); - appendSearchParams(url.searchParams, options); - - const result: GetProjectUsageRequestsResponse = await this.get(this.fetch as Fetch, url); + const requestUrl = this.getRequestUrl(endpoint, { projectId }, options); + const result: GetProjectUsageRequestsResponse = await this.get( + this.fetch as Fetch, + requestUrl + ); return { result, error: null }; } catch (error) { @@ -467,13 +443,14 @@ export class ManageClient extends AbstractRestfulClient { async getProjectUsageRequest( projectId: string, requestId: string, - endpoint = "v1/projects/:projectId/requests/:requestId" + endpoint = ":version/projects/:projectId/requests/:requestId" ): Promise> { try { - const url = new URL(this.baseUrl); - url.pathname = endpoint.replace(/:projectId/, projectId).replace(/:requestId/, requestId); - - const result: GetProjectUsageRequestResponse = await this.get(this.fetch as Fetch, url); + const requestUrl = this.getRequestUrl(endpoint, { projectId, requestId }); + const result: GetProjectUsageRequestResponse = await this.get( + this.fetch as Fetch, + requestUrl + ); return { result, error: null }; } catch (error) { @@ -491,14 +468,14 @@ export class ManageClient extends AbstractRestfulClient { async getProjectUsageSummary( projectId: string, options: GetProjectUsageSummarySchema, - endpoint = "v1/projects/:projectId/usage" + endpoint = ":version/projects/:projectId/usage" ): Promise> { try { - const url = new URL(this.baseUrl); - url.pathname = endpoint.replace(/:projectId/, projectId); - appendSearchParams(url.searchParams, options); - - const result: GetProjectUsageSummaryResponse = await this.get(this.fetch as Fetch, url); + const requestUrl = this.getRequestUrl(endpoint, { projectId }, options); + const result: GetProjectUsageSummaryResponse = await this.get( + this.fetch as Fetch, + requestUrl + ); return { result, error: null }; } catch (error) { @@ -516,14 +493,11 @@ export class ManageClient extends AbstractRestfulClient { async getProjectUsageFields( projectId: string, options: GetProjectUsageFieldsSchema, - endpoint = "v1/projects/:projectId/usage/fields" + endpoint = ":version/projects/:projectId/usage/fields" ): Promise> { try { - const url = new URL(this.baseUrl); - url.pathname = endpoint.replace(/:projectId/, projectId); - appendSearchParams(url.searchParams, options); - - const result: GetProjectUsageFieldsResponse = await this.get(this.fetch as Fetch, url); + const requestUrl = this.getRequestUrl(endpoint, { projectId }, options); + const result: GetProjectUsageFieldsResponse = await this.get(this.fetch as Fetch, requestUrl); return { result, error: null }; } catch (error) { @@ -540,13 +514,11 @@ export class ManageClient extends AbstractRestfulClient { */ async getProjectBalances( projectId: string, - endpoint = "v1/projects/:projectId/balances" + endpoint = ":version/projects/:projectId/balances" ): Promise> { try { - const url = new URL(this.baseUrl); - url.pathname = endpoint.replace(/:projectId/, projectId); - - const result: GetProjectBalancesResponse = await this.get(this.fetch as Fetch, url); + const requestUrl = this.getRequestUrl(endpoint, { projectId }); + const result: GetProjectBalancesResponse = await this.get(this.fetch as Fetch, requestUrl); return { result, error: null }; } catch (error) { @@ -564,13 +536,11 @@ export class ManageClient extends AbstractRestfulClient { async getProjectBalance( projectId: string, balanceId: string, - endpoint = "v1/projects/:projectId/balances/:balanceId" + endpoint = ":version/projects/:projectId/balances/:balanceId" ): Promise> { try { - const url = new URL(this.baseUrl); - url.pathname = endpoint.replace(/:projectId/, projectId).replace(/:balanceId/, balanceId); - - const result: GetProjectBalanceResponse = await this.get(this.fetch as Fetch, url); + const requestUrl = this.getRequestUrl(endpoint, { projectId, balanceId }); + const result: GetProjectBalanceResponse = await this.get(this.fetch as Fetch, requestUrl); return { result, error: null }; } catch (error) { diff --git a/src/packages/OnPremClient.ts b/src/packages/OnPremClient.ts index e8da1ac9..554a8622 100644 --- a/src/packages/OnPremClient.ts +++ b/src/packages/OnPremClient.ts @@ -1,4 +1,4 @@ -import { AbstractRestfulClient } from "./AbstractRestfulClient"; +import { AbstractRestClient } from "./AbstractRestClient"; import { isDeepgramError } from "../lib/errors"; import type { CreateOnPremCredentialsSchema, @@ -9,19 +9,19 @@ import type { OnPremCredentialResponse, } from "../lib/types"; -export class OnPremClient extends AbstractRestfulClient { +export class OnPremClient extends AbstractRestClient { + public namespace: string = "onprem"; + /** * @see https://developers.deepgram.com/reference/list-credentials */ async listCredentials( projectId: string, - endpoint = "v1/projects/:projectId/onprem/distribution/credentials" + endpoint = ":version/projects/:projectId/onprem/distribution/credentials" ): Promise> { try { - const url = new URL(this.baseUrl); - url.pathname = endpoint.replace(/:projectId/, projectId); - - const result: ListOnPremCredentialsResponse = await this.get(this.fetch as Fetch, url); + const requestUrl = this.getRequestUrl(endpoint, { projectId }); + const result: ListOnPremCredentialsResponse = await this.get(this.fetch as Fetch, requestUrl); return { result, error: null }; } catch (error) { @@ -39,15 +39,11 @@ export class OnPremClient extends AbstractRestfulClient { async getCredentials( projectId: string, credentialsId: string, - endpoint = "v1/projects/:projectId/onprem/distribution/credentials/:credentialsId" + endpoint = ":version/projects/:projectId/onprem/distribution/credentials/:credentialsId" ): Promise> { try { - const url = new URL(this.baseUrl); - url.pathname = endpoint - .replace(/:projectId/, projectId) - .replace(/:credentialsId/, credentialsId); - - const result: OnPremCredentialResponse = await this.get(this.fetch as Fetch, url); + const requestUrl = this.getRequestUrl(endpoint, { projectId, credentialsId }); + const result: OnPremCredentialResponse = await this.get(this.fetch as Fetch, requestUrl); return { result, error: null }; } catch (error) { @@ -65,15 +61,17 @@ export class OnPremClient extends AbstractRestfulClient { async createCredentials( projectId: string, options: CreateOnPremCredentialsSchema, - endpoint = "v1/projects/:projectId/onprem/distribution/credentials" + endpoint = ":version/projects/:projectId/onprem/distribution/credentials" ): Promise> { try { - const url = new URL(this.baseUrl); - url.pathname = endpoint.replace(/:projectId/, projectId); - + const requestUrl = this.getRequestUrl(endpoint, { projectId }); const body = JSON.stringify(options); - const result: OnPremCredentialResponse = await this.post(this.fetch as Fetch, url, body); + const result: OnPremCredentialResponse = await this.post( + this.fetch as Fetch, + requestUrl, + body + ); return { result, error: null }; } catch (error) { @@ -91,15 +89,11 @@ export class OnPremClient extends AbstractRestfulClient { async deleteCredentials( projectId: string, credentialsId: string, - endpoint = "v1/projects/:projectId/onprem/distribution/credentials/:credentialsId" + endpoint = ":version/projects/:projectId/onprem/distribution/credentials/:credentialsId" ): Promise> { try { - const url = new URL(this.baseUrl); - url.pathname = endpoint - .replace(/:projectId/, projectId) - .replace(/:credentialsId/, credentialsId); - - const result: MessageResponse = await this.delete(this.fetch as Fetch, url); + const requestUrl = this.getRequestUrl(endpoint, { projectId, credentialsId }); + const result: MessageResponse = await this.delete(this.fetch as Fetch, requestUrl); return { result, error: null }; } catch (error) { diff --git a/src/packages/ReadClient.ts b/src/packages/ReadClient.ts index 09a8c0e8..51e02fe4 100644 --- a/src/packages/ReadClient.ts +++ b/src/packages/ReadClient.ts @@ -1,22 +1,23 @@ -import { AbstractRestfulClient } from "./AbstractRestfulClient"; -import { CallbackUrl, appendSearchParams, isTextSource, isUrlSource } from "../lib/helpers"; +import { AbstractRestClient } from "./AbstractRestClient"; +import { CallbackUrl, isTextSource, isUrlSource } from "../lib/helpers"; import { DeepgramError, isDeepgramError } from "../lib/errors"; import type { AnalyzeSchema, AsyncAnalyzeResponse, DeepgramResponse, Fetch, - PrerecordedSchema, SyncAnalyzeResponse, TextSource, UrlSource, } from "../lib/types"; -export class ReadClient extends AbstractRestfulClient { +export class ReadClient extends AbstractRestClient { + public namespace: string = "read"; + async analyzeUrl( source: UrlSource, options?: AnalyzeSchema, - endpoint = "v1/read" + endpoint = ":version/read" ): Promise> { try { let body; @@ -33,12 +34,8 @@ export class ReadClient extends AbstractRestfulClient { ); } - const analyzeOptions: AnalyzeSchema = { ...{}, ...options }; - - const url = new URL(endpoint, this.baseUrl); - appendSearchParams(url.searchParams, analyzeOptions); - - const result: SyncAnalyzeResponse = await this.post(this.fetch as Fetch, url, body); + const requestUrl = this.getRequestUrl(endpoint, {}, { ...{}, ...options }); + const result: SyncAnalyzeResponse = await this.post(this.fetch as Fetch, requestUrl, body); return { result, error: null }; } catch (error) { @@ -53,7 +50,7 @@ export class ReadClient extends AbstractRestfulClient { async analyzeText( source: TextSource, options?: AnalyzeSchema, - endpoint = "v1/read" + endpoint = ":version/read" ): Promise> { try { let body; @@ -70,12 +67,8 @@ export class ReadClient extends AbstractRestfulClient { ); } - const analyzeOptions: AnalyzeSchema = { ...{}, ...options }; - - const url = new URL(endpoint, this.baseUrl); - appendSearchParams(url.searchParams, analyzeOptions); - - const result: SyncAnalyzeResponse = await this.post(this.fetch as Fetch, url, body); + const requestUrl = this.getRequestUrl(endpoint, {}, { ...{}, ...options }); + const result: SyncAnalyzeResponse = await this.post(this.fetch as Fetch, requestUrl, body); return { result, error: null }; } catch (error) { @@ -91,7 +84,7 @@ export class ReadClient extends AbstractRestfulClient { source: UrlSource, callback: CallbackUrl, options?: AnalyzeSchema, - endpoint = "v1/read" + endpoint = ":version/read" ): Promise> { try { let body; @@ -102,15 +95,12 @@ export class ReadClient extends AbstractRestfulClient { throw new DeepgramError("Unknown source type"); } - const transcriptionOptions: PrerecordedSchema = { - ...options, - ...{ callback: callback.toString() }, - }; - - const url = new URL(endpoint, this.baseUrl); - appendSearchParams(url.searchParams, transcriptionOptions); - - const result: AsyncAnalyzeResponse = await this.post(this.fetch as Fetch, url, body); + const requestUrl = this.getRequestUrl( + endpoint, + {}, + { ...options, callback: callback.toString() } + ); + const result: AsyncAnalyzeResponse = await this.post(this.fetch as Fetch, requestUrl, body); return { result, error: null }; } catch (error) { @@ -126,7 +116,7 @@ export class ReadClient extends AbstractRestfulClient { source: TextSource, callback: CallbackUrl, options?: AnalyzeSchema, - endpoint = "v1/read" + endpoint = ":version/read" ): Promise> { try { let body; @@ -137,15 +127,12 @@ export class ReadClient extends AbstractRestfulClient { throw new DeepgramError("Unknown source type"); } - const transcriptionOptions: PrerecordedSchema = { - ...options, - ...{ callback: callback.toString() }, - }; - - const url = new URL(endpoint, this.baseUrl); - appendSearchParams(url.searchParams, transcriptionOptions); - - const result: AsyncAnalyzeResponse = await this.post(this.fetch as Fetch, url, body, { + const requestUrl = this.getRequestUrl( + endpoint, + {}, + { ...options, callback: callback.toString() } + ); + const result: AsyncAnalyzeResponse = await this.post(this.fetch as Fetch, requestUrl, body, { "Content-Type": "deepgram/audio+video", }); diff --git a/src/packages/SpeakClient.ts b/src/packages/SpeakClient.ts index 9ca8d697..d01e4586 100644 --- a/src/packages/SpeakClient.ts +++ b/src/packages/SpeakClient.ts @@ -1,9 +1,10 @@ -import { AbstractRestfulClient } from "./AbstractRestfulClient"; -import { DeepgramError, DeepgramUnknownError, isDeepgramError } from "../lib/errors"; +import { AbstractRestClient } from "./AbstractRestClient"; import { appendSearchParams, isTextSource } from "../lib/helpers"; +import { DeepgramError, DeepgramUnknownError } from "../lib/errors"; import { Fetch, SpeakSchema, TextSource } from "../lib/types"; -export class SpeakClient extends AbstractRestfulClient { +export class SpeakClient extends AbstractRestClient { + public namespace: string = "speak"; public result: undefined | Response; /** @@ -12,7 +13,7 @@ export class SpeakClient extends AbstractRestfulClient { async request( source: TextSource, options?: SpeakSchema, - endpoint = "v1/speak" + endpoint = ":version/speak" ): Promise { try { let body; diff --git a/src/packages/index.ts b/src/packages/index.ts index 58ad0f9f..8c114f85 100644 --- a/src/packages/index.ts +++ b/src/packages/index.ts @@ -1,8 +1,10 @@ -export { AbstractClient } from "./AbstractClient"; -export { AbstractRestfulClient } from "./AbstractRestfulClient"; -export { AbstractWsClient } from "./AbstractWsClient"; -export { ListenClient } from "./ListenClient"; -export { LiveClient } from "./LiveClient"; -export { ManageClient } from "./ManageClient"; -export { OnPremClient } from "./OnPremClient"; -export { PrerecordedClient } from "./PrerecordedClient"; +export * from "./AbstractClient"; +export * from "./AbstractLiveClient"; +export * from "./AbstractRestClient"; +export * from "./ListenClient"; +export * from "./ListenLiveClient"; +export * from "./ListenRestClient"; +export * from "./ManageClient"; +export * from "./OnPremClient"; +export * from "./ReadClient"; +export * from "./SpeakClient"; diff --git a/test/AbstractClient.test.ts b/test/AbstractClient.test.ts new file mode 100644 index 00000000..4fc8d863 --- /dev/null +++ b/test/AbstractClient.test.ts @@ -0,0 +1,83 @@ +import { AbstractClient } from "../src/packages/AbstractClient"; +import { expect } from "chai"; +import merge from "deepmerge"; +import sinon from "sinon"; +import type { DeepgramClientOptions, DefaultNamespaceOptions } from "../src/lib/types"; + +import { DEFAULT_OPTIONS, DEFAULT_URL } from "../src/lib/constants"; + +class TestClient extends AbstractClient { + constructor(options: DeepgramClientOptions) { + super(options); + } +} + +describe("AbstractClient", () => { + let client: AbstractClient; + const options: DeepgramClientOptions = { + key: "test-key", + global: { + fetch: { options: { url: "https://api.mock.deepgram.com" } }, + websocket: { options: { url: "wss://api.mock.deepgram.com" } }, + }, + }; + + beforeEach(() => { + client = new TestClient(options); + }); + + it("should create an instance of AbstractClient", () => { + expect(client).to.be.an.instanceOf(AbstractClient); + }); + + it("should set the key property correctly", () => { + // @ts-expect-error + expect(client.key).to.equal("test-key"); + }); + + it("should set the options property correctly", () => { + const expectedOptions = merge(DEFAULT_OPTIONS, options); + + // @ts-expect-error + expect(expectedOptions).to.deep.equal(client.options); + }); + + it("should set the namespace property correctly", () => { + expect(client.namespace).to.equal("global"); + }); + + it("should set the version property correctly", () => { + expect(client.version).to.equal("v1"); + }); + + it("should set the baseUrl property correctly", () => { + expect(client.baseUrl).to.equal(DEFAULT_URL); + }); + + it("should set the logger property correctly", () => { + expect(client.logger).to.be.a("function"); + }); + + it("should set the namespaceOptions property correctly", () => { + const expectedOptions = merge(DEFAULT_OPTIONS.global, { ...options.global, key: "test-key" }); + + expect(expectedOptions).to.deep.equal(client.namespaceOptions); + }); + + it("should generate a request URL correctly", () => { + const endpoint = "/:version/transcription"; + const transcriptionOptions = { punctuate: true }; + const expectedUrl = new URL("https://api.deepgram.com/v2/transcription?punctuate=true"); + const actualUrl = client.v("v2").getRequestUrl(endpoint, {}, transcriptionOptions); + expect(actualUrl.toString()).to.equal(expectedUrl.toString()); + }); + + it("should log a message correctly", () => { + const loggerSpy = sinon.spy(client, "logger"); + const kind = "info"; + const msg = "Test message"; + const data = { foo: "bar" }; + client.log(kind, msg, data); + expect(loggerSpy.calledWith(kind, msg, data)).to.be.true; + }); +}); diff --git a/test/AbstractLiveClient.test.ts b/test/AbstractLiveClient.test.ts new file mode 100644 index 00000000..ef8b366c --- /dev/null +++ b/test/AbstractLiveClient.test.ts @@ -0,0 +1,101 @@ +import { expect } from "chai"; +import sinon from "sinon"; +import { AbstractLiveClient } from "../src/packages/AbstractLiveClient"; +import { DeepgramClientOptions } from "../src/lib/types"; +import { CONNECTION_STATE, SOCKET_STATES } from "../src/lib/constants"; + +class TestLiveClient extends AbstractLiveClient { + constructor(options: DeepgramClientOptions) { + super(options); + } + + setupConnection(): void { + // Dummy implementation + } +} + +describe("AbstractLiveClient", () => { + let liveClient: TestLiveClient; + const options: DeepgramClientOptions = { + key: "test-key", + global: { + websocket: { + options: { + url: "wss://api.mock.deepgram.com", + }, + }, + }, + }; + + beforeEach(() => { + liveClient = new TestLiveClient(options); + }); + + it("should set the URL property correctly", () => { + expect(liveClient.baseUrl).to.equal("wss://api.mock.deepgram.com"); + }); + + it("should include the Authorization header", () => { + expect(liveClient.headers).to.have.property("Authorization", "Token test-key"); + }); + + it("should connect the socket", (done) => { + // @ts-expect-error + const connectSpy = sinon.spy(liveClient, "connect"); + const transcriptionOptions = { punctuate: true }; + const endpoint = "/v1/listen"; + // @ts-expect-error + liveClient.connect(transcriptionOptions, endpoint); + expect(connectSpy.calledOnce).to.be.true; + done(); + }); + + it("should reconnect the socket", () => { + const reconnectSpy = sinon.spy(liveClient, "reconnect"); + const transcriptionOptions = { numerals: true }; + liveClient.reconnect(transcriptionOptions); + expect(reconnectSpy.calledOnce).to.be.true; + }); + + it("should disconnect the socket", () => { + const disconnectSpy = sinon.spy(liveClient, "disconnect"); + liveClient.disconnect(); + expect(disconnectSpy.calledOnce).to.be.true; + }); + + it("should return the correct connection state", () => { + expect(liveClient.connectionState()).to.equal(CONNECTION_STATE.Closed); + // @ts-expect-error + liveClient.conn = { readyState: SOCKET_STATES.connecting }; + expect(liveClient.connectionState()).to.equal(CONNECTION_STATE.Connecting); + // @ts-expect-error + liveClient.conn = { readyState: SOCKET_STATES.open }; + expect(liveClient.connectionState()).to.equal(CONNECTION_STATE.Open); + // @ts-expect-error + liveClient.conn = { readyState: SOCKET_STATES.closing }; + expect(liveClient.connectionState()).to.equal(CONNECTION_STATE.Closing); + }); + + it("should return the correct ready state", () => { + expect(liveClient.getReadyState()).to.equal(SOCKET_STATES.closed); + // @ts-expect-error + liveClient.conn = { readyState: SOCKET_STATES.connecting }; + expect(liveClient.getReadyState()).to.equal(SOCKET_STATES.connecting); + }); + + it("should check if the connection is open", () => { + expect(liveClient.isConnected()).to.be.false; + // @ts-expect-error + liveClient.conn = { readyState: SOCKET_STATES.open }; + expect(liveClient.isConnected()).to.be.true; + }); + + it("should send data to the Deepgram API", () => { + const sendSpy = sinon.spy(liveClient, "send"); + const data = new Blob(["test data"]); + // @ts-expect-error + liveClient.conn = { send: () => {} }; + liveClient.send(data); + expect(sendSpy.calledOnce).to.be.true; + }); +}); diff --git a/test/AbstractRestClient.test.ts b/test/AbstractRestClient.test.ts new file mode 100644 index 00000000..a9c62879 --- /dev/null +++ b/test/AbstractRestClient.test.ts @@ -0,0 +1,217 @@ +import { expect } from "chai"; +import sinon from "sinon"; +import { AbstractRestClient } from "../src/packages/AbstractRestClient"; +import { DeepgramClientOptions } from "../src/lib/types"; +import { DeepgramError, DeepgramApiError, DeepgramUnknownError } from "../src/lib/errors"; +import * as helpers from "../src/lib/helpers"; + +class TestRestClient extends AbstractRestClient { + constructor(options: DeepgramClientOptions) { + super(options); + } +} + +describe("AbstractRestClient", () => { + let restClient: TestRestClient; + const options: DeepgramClientOptions = { + key: "test-key", + global: { + fetch: { + options: { + url: "https://api.mock.deepgram.com", + }, + }, + }, + }; + + beforeEach(() => { + restClient = new TestRestClient(options); + }); + + it("should throw an error when running in the browser without a proxy", () => { + const isBrowserStub = sinon.stub(helpers, "isBrowser"); + isBrowserStub.returns(true); + expect(() => new TestRestClient(options)).to.throw(DeepgramError); + isBrowserStub.restore(); + }); + + it("should set the baseUrl correctly with a proxy", () => { + const options: DeepgramClientOptions = { + key: "proxy", + global: { + fetch: { + options: { + url: "https://api.mock.deepgram.com", + proxy: { + url: "https://proxy.mock.deepgram.com", + }, + }, + }, + }, + }; + + const restClient = new TestRestClient(options); + expect(restClient.baseUrl).to.equal("https://proxy.mock.deepgram.com"); + }); + + it("should set the baseUrl correctly without a proxy", () => { + expect(restClient.baseUrl).to.equal("https://api.mock.deepgram.com"); + }); + + it("should handle API errors correctly", async () => { + const mockResponse = new Response(JSON.stringify({ error: "Bad Request" }), { + status: 400, + }); + const fetchStub = sinon.stub().rejects(mockResponse); + // @ts-expect-error + const handleErrorSpy = sinon.spy(restClient, "_handleError"); + + try { + // @ts-expect-error + await restClient._handleRequest(fetchStub, "GET", "https://api.mock.deepgram.com"); + } catch (error: any) { + expect(handleErrorSpy.calledOnce).to.be.true; + expect(error).to.be.an.instanceOf(DeepgramApiError); + expect(error.message).to.equal("Bad Request"); + expect(error.status).to.equal(400); + } + }); + + it("should handle unknown errors correctly", async () => { + const mockError = new Error("Unknown error"); + const fetchStub = sinon.stub().rejects(mockError); + // @ts-expect-error + const handleErrorSpy = sinon.spy(restClient, "_handleError"); + + try { + // @ts-expect-error + await restClient._handleRequest(fetchStub, "GET", "https://api.mock.deepgram.com"); + } catch (error: any) { + expect(handleErrorSpy.calledOnce).to.be.true; + expect(error).to.be.an.instanceOf(DeepgramUnknownError); + expect(error.message).to.equal("Unknown error"); + } + }); + + it("should get request parameters correctly", () => { + const headers = { "Content-Type": "application/json", Authorization: "Token test-key" }; + // @ts-expect-error + const parameters: FetchParameters = { cache: "no-cache" }; + const body = JSON.stringify({ data: "test" }); + + // @ts-expect-error + const getParams = restClient._getRequestParams("GET", headers, parameters); + expect(getParams).to.deep.equal({ + method: "GET", + headers: { + Authorization: "Token test-key", + "Content-Type": "application/json", + }, + }); + + // @ts-expect-error + const postParams = restClient._getRequestParams("POST", headers, parameters, body); + expect(postParams).to.deep.equal({ + method: "POST", + headers: { + Authorization: "Token test-key", + "Content-Type": "application/json", + }, + body: body, + duplex: "half", + cache: "no-cache", + }); + }); + + it("should handle successful requests correctly", async () => { + const mockResponse = { + ok: true, + json: () => Promise.resolve({ data: "test" }), + }; + const fetchStub = sinon.stub().resolves(mockResponse); + + // @ts-expect-error + const result = await restClient._handleRequest( + fetchStub, + "GET", + "https://api.mock.deepgram.com" + ); + expect(result).to.deep.equal({ data: "test" }); + }); + + it("should handle raw requests correctly", async () => { + const mockResponse = { ok: true, text: () => Promise.resolve("test data") }; + const fetchStub = sinon.stub().resolves(mockResponse); + + // @ts-expect-error + const result = await restClient._handleRawRequest( + fetchStub, + "GET", + "https://api.mock.deepgram.com" + ); + expect(result).to.deep.equal(mockResponse); + }); + + it("should make GET requests correctly", async () => { + const mockResponse = { + ok: true, + json: () => Promise.resolve({ data: "test" }), + }; + const fetchStub = sinon.stub().resolves(mockResponse); + + // @ts-expect-error + const result = await restClient.get(fetchStub, "https://api.mock.deepgram.com"); + expect(result).to.deep.equal({ data: "test" }); + }); + + it("should make POST requests correctly", async () => { + const mockResponse = { + ok: true, + json: () => Promise.resolve({ data: "test" }), + }; + const fetchStub = sinon.stub().resolves(mockResponse); + const body = JSON.stringify({ data: "test" }); + + // @ts-expect-error + const result = await restClient.post(fetchStub, "https://api.mock.deepgram.com", body); + expect(result).to.deep.equal({ data: "test" }); + }); + + it("should make PUT requests correctly", async () => { + const mockResponse = { + ok: true, + json: () => Promise.resolve({ data: "test" }), + }; + const fetchStub = sinon.stub().resolves(mockResponse); + const body = JSON.stringify({ data: "test" }); + + // @ts-expect-error + const result = await restClient.put(fetchStub, "https://api.mock.deepgram.com", body); + expect(result).to.deep.equal({ data: "test" }); + }); + + it("should make PATCH requests correctly", async () => { + const mockResponse = { + ok: true, + json: () => Promise.resolve({ data: "test" }), + }; + const fetchStub = sinon.stub().resolves(mockResponse); + const body = JSON.stringify({ data: "test" }); + + // @ts-expect-error + const result = await restClient.patch(fetchStub, "https://api.mock.deepgram.com", body); + expect(result).to.deep.equal({ data: "test" }); + }); + + it("should make DELETE requests correctly", async () => { + const mockResponse = { + ok: true, + json: () => Promise.resolve({ data: "test" }), + }; + const fetchStub = sinon.stub().resolves(mockResponse); + + // @ts-expect-error + const result = await restClient.delete(fetchStub, "https://api.mock.deepgram.com"); + expect(result).to.deep.equal({ data: "test" }); + }); +}); diff --git a/test/ListenLiveClient.test.ts b/test/ListenLiveClient.test.ts new file mode 100644 index 00000000..60fd4670 --- /dev/null +++ b/test/ListenLiveClient.test.ts @@ -0,0 +1,75 @@ +import { expect } from "chai"; +import sinon from "sinon"; +import { ListenLiveClient } from "../src/packages/ListenLiveClient"; +import { DeepgramClientOptions, LiveConfigOptions } from "../src/lib/types"; +import { LiveTranscriptionEvents } from "../src/lib/enums"; + +describe("ListenLiveClient", () => { + let liveClient: ListenLiveClient; + const options: DeepgramClientOptions = { + key: "test-key", + global: { + websocket: { + options: { + url: "wss://api.mock.deepgram.com", + }, + }, + }, + }; + + beforeEach(() => { + liveClient = new ListenLiveClient(options); + }); + + it("should set the namespace property correctly", () => { + expect(liveClient.namespace).to.equal("listen"); + }); + + it("should set up the connection event handlers", () => { + const eventSpy = sinon.spy(liveClient, "emit"); + + liveClient.setupConnection(); + + // Simulate connection events + // @ts-expect-error + liveClient.conn.onopen(); + // @ts-expect-error + liveClient.conn.onclose({}); + // @ts-expect-error + liveClient.conn.onmessage({ + data: JSON.stringify({ + type: LiveTranscriptionEvents.Transcript, + data: {}, + }), + }); + + expect(eventSpy.calledWith(LiveTranscriptionEvents.Open, liveClient)).to.be.true; + expect(eventSpy.calledWith(LiveTranscriptionEvents.Close, {})).to.be.true; + expect( + eventSpy.calledWith(LiveTranscriptionEvents.Transcript, { + type: LiveTranscriptionEvents.Transcript, + data: {}, + }) + ).to.be.true; + }); + + it("should configure the live client", () => { + const sendSpy = sinon.spy(liveClient, "send"); + const config: LiveConfigOptions = { numerals: true }; + liveClient.configure(config); + expect(sendSpy.calledWith(JSON.stringify({ type: "Configure", processors: config }))).to.be + .true; + }); + + it("should send a KeepAlive message", () => { + const sendSpy = sinon.spy(liveClient, "send"); + liveClient.keepAlive(); + expect(sendSpy.calledWith(JSON.stringify({ type: "KeepAlive" }))).to.be.true; + }); + + it("should request to close the connection", () => { + const sendSpy = sinon.spy(liveClient, "send"); + liveClient.requestClose(); + expect(sendSpy.calledWith(JSON.stringify({ type: "CloseStream" }))).to.be.true; + }); +}); diff --git a/test/client.test.ts b/test/client.test.ts deleted file mode 100644 index 426edf8c..00000000 --- a/test/client.test.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { createClient } from "../src"; -import { DEFAULT_URL } from "../src/lib/constants"; -import { expect, assert } from "chai"; -import { faker } from "@faker-js/faker"; -import { stripTrailingSlash } from "../src/lib/helpers"; -import DeepgramClient from "../src/DeepgramClient"; -import { ListenClient } from "../src/packages/ListenClient"; - -const deepgram = createClient(faker.string.alphanumeric(40)); - -describe("testing creation of a deepgram client object", () => { - it("it should create the client object", () => { - expect(deepgram).to.not.be.undefined; - expect(deepgram).is.instanceOf(DeepgramClient); - }); - - it("it should provide provide access to a transcription client", () => { - expect(deepgram.listen).to.not.be.undefined; - expect(deepgram.listen).is.instanceOf(ListenClient); - }); - - it("it should have the default URL when no custom URL option is provided", () => { - // @ts-ignore - const url = deepgram.baseUrl.hostname; - - expect(url).to.equal(DEFAULT_URL); - }); - - it("it should throw an error if no valid apiKey is provided", () => { - expect(() => createClient("")).to.throw("A deepgram API key is required"); - }); - - it("it should throw an error if invalid options are provided", () => { - expect(() => createClient(faker.string.alphanumeric(40), { global: { url: "" } })).to.throw( - `An API URL is required. It should be set to ${DEFAULT_URL} by default. No idea what happened!` - ); - }); - - it("it should create the client object with a custom domain", () => { - const domain = faker.internet.url({ appendSlash: false }); - const client = createClient(faker.string.alphanumeric(40), { - global: { url: domain }, - }); - - // @ts-ignore - const baseUrl = client.baseUrl; - - expect(client).is.instanceOf(DeepgramClient); - expect(`${baseUrl.protocol}//${baseUrl.hostname}`).to.equal(domain); - }); - - it("it should strip trailing slashes off the API URL if they're supplied", () => { - const domain = faker.internet.url({ appendSlash: true }); - const client = createClient(faker.string.alphanumeric(40), { - global: { url: domain }, - }); - - // @ts-ignore - const baseUrl = client.baseUrl; - - expect(client).is.instanceOf(DeepgramClient); - expect(`${baseUrl.protocol}//${baseUrl.hostname}`).to.equal(stripTrailingSlash(domain)); - }); - - it("it should still work when provided a URL without a protocol", () => { - const domain = `api.mock.deepgram.com`; - const client = createClient(faker.string.alphanumeric(40), { - global: { url: domain }, - }); - - // @ts-ignore - const url = client.baseUrl.hostname; - - expect(client).is.instanceOf(DeepgramClient); - expect(url).to.equal("api.mock.deepgram.com"); - }); - - it("it should allow for the supply of a custom header", () => { - const client = createClient(faker.string.alphanumeric(40), { - global: { headers: { "X-dg-test": "testing" } }, - }); - - expect(client).is.instanceOf(DeepgramClient); - }); - - it("should use custom fetch when provided", async () => { - const fetch = async () => { - return new Response(JSON.stringify({ customFetch: true })); - }; - - const client = createClient(faker.string.alphanumeric(40), { - global: { url: "https://api.mock.deepgram.com" }, - _experimentalCustomFetch: fetch, - }); - - const { result, error } = await client.manage.getProjectBalances(faker.string.uuid()); - - assert.isNull(error); - assert.isNotNull(result); - assert.containsAllDeepKeys(result, ["customFetch"]); - }); -}); diff --git a/test/constants.test.ts b/test/constants.test.ts deleted file mode 100644 index 1bae3f82..00000000 --- a/test/constants.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { applySettingDefaults } from "../src/lib/helpers"; -import { DeepgramClientOptions } from "../src/lib/types/DeepgramClientOptions"; -import { DEFAULT_OPTIONS } from "../src/lib/constants"; -import { expect } from "chai"; -import { faker } from "@faker-js/faker"; - -describe("testing constants", () => { - it("DEFAULT_OPTIONS are valid options", () => { - const options: DeepgramClientOptions = DEFAULT_OPTIONS; - - expect(options).to.not.be.undefined; - }); - - it("DEFAULT_OPTIONS can be overridden", () => { - const options = { - global: { url: faker.internet.url({ appendSlash: false }) }, - }; - const settings = applySettingDefaults(options, DEFAULT_OPTIONS); - - expect(settings).is.not.deep.equal(options); - }); -}); diff --git a/test/errors.test.ts b/test/errors.test.ts deleted file mode 100644 index 61d77743..00000000 --- a/test/errors.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { expect } from "chai"; -import { - DeepgramApiError, - DeepgramError, - DeepgramUnknownError, - isDeepgramError, -} from "../src/lib/errors"; - -describe("testing errors", () => { - it("we can create an API error", () => { - const error = new DeepgramError("Testing an error"); - expect(error).to.not.be.undefined; - expect(isDeepgramError(error)).equals(true); - expect(error).is.instanceOf(DeepgramError); - }); - - it("an API error will convert to JSON", () => { - const error = new DeepgramApiError("Testing an error", 400); - expect(JSON.stringify(error)).equals( - '{"name":"DeepgramApiError","message":"Testing an error","status":400}' - ); - expect(error).is.instanceOf(DeepgramApiError); - }); - - it("an unknown error is still an error", () => { - const error = new Error("Testing an error"); - const dgError = new DeepgramUnknownError("Unknown error test", error); - expect(isDeepgramError(dgError)).equals(true); - expect(dgError).is.instanceOf(DeepgramUnknownError); - }); -}); diff --git a/test/helpers.test.ts b/test/helpers.test.ts deleted file mode 100644 index e35e5b27..00000000 --- a/test/helpers.test.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { expect } from "chai"; -import { faker } from "@faker-js/faker"; -import { applySettingDefaults, stripTrailingSlash } from "../src/lib/helpers"; -import { DEFAULT_OPTIONS } from "../src/lib/constants"; - -describe("testing helpers", () => { - it("it should strip the trailing slash from a URL", () => { - const URL = faker.internet.url({ appendSlash: true }); - const expectedURL = URL.slice(0, -1); - expect(stripTrailingSlash(URL)).to.equal(expectedURL); - }); - - it("it should override defaults with options provided", () => { - const options = JSON.parse(JSON.stringify(DEFAULT_OPTIONS)); // deep copy DEFAULT_OPTIONS - options.global.url = faker.internet.url({ appendSlash: false }); - expect(applySettingDefaults(options, DEFAULT_OPTIONS)).to.deep.equal(options); - }); -}); diff --git a/test/legacy.test.ts b/test/legacy.test.ts deleted file mode 100644 index ecb1f872..00000000 --- a/test/legacy.test.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { assert, expect } from "chai"; -import { createClient, Deepgram, DeepgramVersionError } from "../src"; -import { faker } from "@faker-js/faker"; -import DeepgramClient from "../src/DeepgramClient"; - -const errorText = - "You are attempting to use an old format for a newer SDK version. Read more here: https://dpgr.am/js-v3"; - -describe("legacy error handling", () => { - let deepgram: DeepgramClient; - - beforeEach(() => { - deepgram = createClient(faker.string.alphanumeric(40), { - global: { url: "https://api.mock.deepgram.com" }, - }); - }); - - it("should create the correct client object", () => { - expect(deepgram).to.not.be.undefined; - expect(deepgram).is.instanceOf(DeepgramClient); - }); - - it("should error when using a v2 client object", async () => { - assert.throw( - () => { - new Deepgram(faker.string.alphanumeric(40)); - }, - DeepgramVersionError, - errorText - ); - }); - - it("should error when using an old v2 callstack for transcription", async () => { - assert.throw( - () => { - deepgram.transcription.preRecorded( - { - url: "https://dpgr.am/spacewalk.wav", - }, - { - model: "nova", - callback: "http://callback/endpoint", - } - ); - }, - DeepgramVersionError, - errorText - ); - }); - - it("should error when using an old v2 callstack for projects", async () => { - assert.throw( - () => { - deepgram.projects.list(); - }, - DeepgramVersionError, - errorText - ); - }); - - it("should error when using an old v2 callstack for keys", async () => { - assert.throw( - () => { - deepgram.keys.list("projectId"); - }, - DeepgramVersionError, - errorText - ); - }); - - it("should error when using an old v2 callstack for members", async () => { - assert.throw( - () => { - deepgram.members.listMembers("projectId"); - }, - DeepgramVersionError, - errorText - ); - }); - - it("should error when using an old v2 callstack for scopes", async () => { - assert.throw( - () => { - deepgram.scopes.get("projectId", "projectMemberId"); - }, - DeepgramVersionError, - errorText - ); - }); - - it("should error when using an old v2 callstack for invitation", async () => { - assert.throw( - () => { - deepgram.invitation.list("projectId"); - }, - DeepgramVersionError, - errorText - ); - }); - - it("should error when using an old v2 callstack for usage", async () => { - assert.throw( - () => { - deepgram.usage.listRequests("projectId", {}); - }, - DeepgramVersionError, - errorText - ); - }); - - it("should error when using an old v2 callstack for billing", async () => { - assert.throw( - () => { - deepgram.billing.listBalances("projectId"); - }, - DeepgramVersionError, - errorText - ); - }); -}); diff --git a/test/live.test.ts b/test/live.test.ts deleted file mode 100644 index b777af29..00000000 --- a/test/live.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { assert, expect } from "chai"; -import { createClient } from "../src"; -import { faker } from "@faker-js/faker"; -import DeepgramClient from "../src/DeepgramClient"; -import { LiveConnectionState, LiveTranscriptionEvents } from "../src/lib/enums"; - -describe("connecting to our transcription websocket", () => { - let deepgram: DeepgramClient; - - beforeEach(() => { - deepgram = createClient(faker.string.alphanumeric(40), { - global: { url: "https://api.mock.deepgram.com" }, - }); - }); - - it("should create the client object", () => { - expect(deepgram).to.not.be.undefined; - expect(deepgram).is.instanceOf(DeepgramClient); - }); - - it("should connect to the websocket", function (done) { - const connection = deepgram.listen.live({ model: "general", tier: "enhanced" }); - - connection.on(LiveTranscriptionEvents.Open, (event) => { - expect(connection.getReadyState()).to.eq(LiveConnectionState.OPEN); - - connection.on(LiveTranscriptionEvents.Metadata, (data) => { - assert.isNotNull(data); - assert.containsAllDeepKeys(data, ["request_id"]); - - connection.finish(); - done(); - }); - }); - }); - - it("should send data and recieve a transcription object back", function (done) { - const connection = deepgram.listen.live({ model: "general", tier: "enhanced" }); - - connection.on(LiveTranscriptionEvents.Open, () => { - connection.on(LiveTranscriptionEvents.Metadata, (data) => { - assert.isNotNull(data); - assert.containsAllDeepKeys(data, ["request_id"]); - }); - - connection.on(LiveTranscriptionEvents.Transcript, (data) => { - assert.isNotNull(data); - assert.containsAllDeepKeys(data, ["channel"]); - - connection.finish(); - done(); - }); - - connection.send(new Uint8Array(100)); // mock ArrayBufferLike audio data - }); - }); - - it("should receive a warning if trying to send zero-byte length data", function (done) { - const connection = deepgram.listen.live({ model: "general", tier: "enhanced" }); - - connection.on(LiveTranscriptionEvents.Open, () => { - connection.on(LiveTranscriptionEvents.Warning, (data) => { - assert.isNotNull(data); - - expect(data).to.eq( - "Zero-byte detected, skipping. Send `CloseStream` if trying to close the connection." - ); - - connection.finish(); - done(); - }); - - connection.send(new Uint8Array(0)); - }); - }); -}); diff --git a/test/manage.test.ts b/test/manage.test.ts deleted file mode 100644 index 7918d220..00000000 --- a/test/manage.test.ts +++ /dev/null @@ -1,231 +0,0 @@ -import { assert, expect } from "chai"; -import { createClient } from "../src"; -import { faker } from "@faker-js/faker"; -import DeepgramClient from "../src/DeepgramClient"; - -describe("making manage requests", () => { - let deepgram: DeepgramClient; - - beforeEach(() => { - deepgram = createClient(faker.string.alphanumeric(40), { - global: { url: "https://api.mock.deepgram.com" }, - }); - }); - - it("should create the client object", () => { - expect(deepgram).to.not.be.undefined; - expect(deepgram).is.instanceOf(DeepgramClient); - }); - - it("should get all projects for a user", async () => { - const { result, error } = await deepgram.manage.getProjects(); - - assert.isNull(error); - assert.isNotNull(result); - assert.containsAllDeepKeys(result, ["projects"]); - }); - - it("should get a project", async () => { - const { result, error } = await deepgram.manage.getProject(faker.string.uuid()); - - assert.isNull(error); - assert.isNotNull(result); - assert.containsAllDeepKeys(result, ["project_id"]); - }); - - it("should update a project", async () => { - const { result, error } = await deepgram.manage.updateProject(faker.string.uuid(), { - name: "test", - }); - - assert.isNull(error); - assert.isNotNull(result); - assert.containsAllDeepKeys(result, ["message"]); - }); - - it("should delete a project", async () => { - const { error } = await deepgram.manage.deleteProject(faker.string.uuid()); - - assert.isNull(error); - }); - - it("should get all project key details", async () => { - const { result, error } = await deepgram.manage.getProjectKeys(faker.string.uuid()); - - assert.isNull(error); - assert.isNotNull(result); - assert.containsAllDeepKeys(result, ["api_keys"]); - }); - - it("should get a project key", async () => { - const { result, error } = await deepgram.manage.getProjectKey( - faker.string.uuid(), - faker.string.uuid() - ); - - assert.isNull(error); - assert.isNotNull(result); - assert.containsAllDeepKeys(result?.api_key, ["api_key_id"]); - }); - - it("should create a project key", async () => { - const { result, error } = await deepgram.manage.createProjectKey(faker.string.uuid(), { - comment: faker.lorem.words(4), - scopes: [faker.lorem.word()], - }); - - assert.isNull(error); - assert.isNotNull(result); - assert.containsAllDeepKeys(result, ["key"]); - }); - - it("should delete a project key", async () => { - const { error } = await deepgram.manage.deleteProjectKey( - faker.string.uuid(), - faker.string.uuid() - ); - - assert.isNull(error); - }); - - it("should get all project members", async () => { - const { result, error } = await deepgram.manage.getProjectMembers(faker.string.uuid()); - - assert.isNull(error); - assert.isNotNull(result); - assert.containsAllDeepKeys(result, ["members"]); - }); - - it("should remove a project member", async () => { - const { error } = await deepgram.manage.removeProjectMember( - faker.string.uuid(), - faker.string.uuid() - ); - - assert.isNull(error); - }); - - it("should get all scopes for a project member", async () => { - const { result, error } = await deepgram.manage.getProjectMemberScopes( - faker.string.uuid(), - faker.string.uuid() - ); - - assert.isNull(error); - assert.isNotNull(result); - assert.containsAllDeepKeys(result, ["scopes"]); - }); - - it("should update a scope for a project member", async () => { - const { result, error } = await deepgram.manage.updateProjectMemberScope( - faker.string.uuid(), - faker.string.uuid(), - { - scope: faker.lorem.word(), - } - ); - - assert.isNull(error); - assert.isNotNull(result); - assert.containsAllDeepKeys(result, ["message"]); - }); - - it("should get all project invites", async () => { - const { result, error } = await deepgram.manage.getProjectInvites(faker.string.uuid()); - - assert.isNull(error); - assert.isNotNull(result); - assert.containsAllDeepKeys(result, ["invites"]); - }); - - it("should send a project invite", async () => { - const { result, error } = await deepgram.manage.sendProjectInvite(faker.string.uuid(), { - email: faker.internet.email(), - scope: faker.lorem.word(), - }); - - assert.isNull(error); - assert.isNotNull(result); - assert.containsAllDeepKeys(result, ["message"]); - }); - - it("should delete a project invite", async () => { - const { error } = await deepgram.manage.deleteProjectInvite( - faker.string.uuid(), - faker.internet.email() - ); - - assert.isNull(error); - }); - - it("should leave a project", async () => { - const { result, error } = await deepgram.manage.leaveProject(faker.string.uuid()); - - assert.isNull(error); - assert.isNotNull(result); - assert.containsAllDeepKeys(result, ["message"]); - }); - - it("should get all usage requests for a project", async () => { - const { result, error } = await deepgram.manage.getProjectUsageRequests(faker.string.uuid(), { - start: faker.date.anytime().toISOString(), - end: faker.date.anytime().toISOString(), - }); - - assert.isNull(error); - assert.isNotNull(result); - assert.containsAllDeepKeys(result, ["requests"]); - }); - - it("should get a usage request for a project", async () => { - const { result, error } = await deepgram.manage.getProjectUsageRequest( - faker.string.uuid(), - faker.string.uuid() - ); - - assert.isNull(error); - assert.isNotNull(result); - assert.containsAllDeepKeys(result, ["response"]); - }); - - it("should get the project usage summary", async () => { - const { result, error } = await deepgram.manage.getProjectUsageSummary(faker.string.uuid(), { - start: faker.date.anytime().toISOString(), - end: faker.date.anytime().toISOString(), - }); - - assert.isNull(error); - assert.isNotNull(result); - assert.containsAllDeepKeys(result, ["results"]); - }); - - it("should get project usage fields", async () => { - const { result, error } = await deepgram.manage.getProjectUsageFields(faker.string.uuid(), { - start: faker.date.anytime().toISOString(), - end: faker.date.anytime().toISOString(), - }); - - assert.isNull(error); - assert.isNotNull(result); - assert.containsAllDeepKeys(result, ["models"]); - }); - - it("should get all project balances", async () => { - const { result, error } = await deepgram.manage.getProjectBalances(faker.string.uuid()); - - assert.isNull(error); - assert.isNotNull(result); - assert.containsAllDeepKeys(result, ["balances"]); - }); - - it("should get a project balance", async () => { - const { result, error } = await deepgram.manage.getProjectBalance( - faker.string.uuid(), - faker.string.uuid() - ); - - assert.isNull(error); - assert.isNotNull(result); - assert.containsAllDeepKeys(result, ["balance_id"]); - }); -}); diff --git a/test/onprem.test.ts b/test/onprem.test.ts deleted file mode 100644 index 5a5a4e1c..00000000 --- a/test/onprem.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { assert, expect } from "chai"; -import { createClient } from "../src"; -import { faker } from "@faker-js/faker"; -import DeepgramClient from "../src/DeepgramClient"; - -describe("making onprem requests", () => { - let deepgram: DeepgramClient; - - beforeEach(() => { - deepgram = createClient(faker.string.alphanumeric(40), { - global: { url: "https://api.mock.deepgram.com" }, - }); - }); - - it("should create the client object", () => { - expect(deepgram).to.not.be.undefined; - expect(deepgram).is.instanceOf(DeepgramClient); - }); - - it("should list onprem credentials", async () => { - const { result, error } = await deepgram.onprem.listCredentials(faker.string.uuid()); - - assert.isNull(error); - assert.isNotNull(result); - assert.containsAllDeepKeys(result, ["distribution_credentials"]); - }); - - it("should get onprem credentials", async () => { - const { result, error } = await deepgram.onprem.getCredentials( - faker.string.uuid(), - faker.string.uuid() - ); - - assert.isNull(error); - assert.isNotNull(result); - assert.containsAllDeepKeys(result, ["member"]); - }); - - it("should create onprem credentials", async () => { - const { result, error } = await deepgram.onprem.createCredentials(faker.string.uuid(), { - comment: faker.lorem.paragraph(), - scopes: [faker.lorem.word()], - provider: faker.lorem.word(), - }); - - assert.isNull(error); - assert.isNotNull(result); - assert.containsAllDeepKeys(result, ["member"]); - }); - - it("should delete onprem credentials", async () => { - const { result, error } = await deepgram.onprem.deleteCredentials( - faker.string.uuid(), - faker.string.uuid() - ); - - assert.isNull(error); - assert.isNotNull(result); - assert.containsAllDeepKeys(result, ["message"]); - }); -}); diff --git a/test/prerecorded.test.ts b/test/prerecorded.test.ts deleted file mode 100644 index e726bf2a..00000000 --- a/test/prerecorded.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { assert, expect } from "chai"; -import { createClient } from "../src"; -import { faker } from "@faker-js/faker"; -import DeepgramClient from "../src/DeepgramClient"; -import { CallbackUrl } from "../src/lib/helpers"; -import { UrlSource } from "../src/lib/types"; - -const bufferSource: Buffer = Buffer.from("string"); - -const urlSource: UrlSource = { - url: faker.internet.url({ appendSlash: false }) + "/nasa.wav", -}; - -describe("making listen requests", () => { - let deepgram: DeepgramClient; - - beforeEach(() => { - deepgram = createClient(faker.string.alphanumeric(40), { - global: { url: "https://api.mock.deepgram.com" }, - }); - }); - - it("should create the client object", () => { - expect(deepgram).to.not.be.undefined; - expect(deepgram).is.instanceOf(DeepgramClient); - }); - - it("should transcribe a URL source synchronously", async () => { - const { result, error } = await deepgram.listen.prerecorded.transcribeUrl(urlSource); - - assert.isNull(error); - assert.isNotNull(result); - assert.containsAllDeepKeys(result?.metadata, ["request_id"]); - }); - - it("should transcribe a file source synchronously", async () => { - const { result, error } = await deepgram.listen.prerecorded.transcribeFile(bufferSource); - - assert.isNull(error); - assert.isNotNull(result); - assert.containsAllDeepKeys(result?.metadata, ["request_id"]); - }); - - it("should transcribe a URL source asynchronously", async () => { - const { result, error } = await deepgram.listen.prerecorded.transcribeUrlCallback( - urlSource, - new CallbackUrl("https://example.com/callback") - ); - - assert.isNull(error); - assert.isNotNull(result); - assert.containsAllDeepKeys(result, ["request_id"]); - }); - - it("should transcribe a file source asynchronously", async () => { - const { result, error } = await deepgram.listen.prerecorded.transcribeFileCallback( - bufferSource, - new CallbackUrl("https://example.com/callback") - ); - - assert.isNull(error); - assert.isNotNull(result); - assert.containsAllDeepKeys(result, ["request_id"]); - }); -}); diff --git a/webpack.config.js b/webpack.config.js index 04a8b84c..a6c95016 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,4 +1,5 @@ const path = require("path"); +const webpack = require("webpack"); module.exports = { entry: "./src/index.ts", @@ -24,4 +25,9 @@ module.exports = { resolve: { extensions: [".ts", ".js", ".json"], }, + plugins: [ + new webpack.DefinePlugin({ + "process.versions.node": JSON.stringify(process.versions.node), + }), + ], };