diff --git a/eslint.config.ts b/eslint.config.ts index 77975224..266b2a68 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -1,11 +1,25 @@ import { globalIgnores } from 'eslint/config' import importPlugin from 'eslint-plugin-import' +import jsdoc from 'eslint-plugin-jsdoc' import simpleImportSort from 'eslint-plugin-simple-import-sort' import unusedImports from 'eslint-plugin-unused-imports' import neostandard, { resolveIgnoresFromGitignore } from 'neostandard' const files = ['**/*.{ts,tsx}'] +// Documentation is required only for the core client modules +// covering the concepts in the README: +// client options, authentication, action attempts, pagination, +// requests, and errors. +const publicApiFiles = [ + 'src/lib/options.ts', + 'src/lib/request-options.ts', + 'src/lib/resolve-action-attempt.ts', + 'src/lib/seam-http-error.ts', + 'src/lib/seam-http-request.ts', + 'src/lib/seam-paginator.ts', +] + export default [ globalIgnores(resolveIgnoresFromGitignore()), ...neostandard({ ts: true, noStyle: true }), @@ -73,6 +87,51 @@ export default [ ], }, }, + { + ...jsdoc.configs['flat/recommended-typescript-error'], + files: publicApiFiles, + rules: { + ...jsdoc.configs['flat/recommended-typescript-error']?.rules, + 'jsdoc/require-jsdoc': [ + 'error', + { + publicOnly: true, + require: { + ArrowFunctionExpression: true, + ClassDeclaration: true, + ClassExpression: true, + FunctionDeclaration: true, + FunctionExpression: true, + MethodDefinition: false, + }, + contexts: [ + 'ExportNamedDeclaration > TSInterfaceDeclaration', + 'ExportNamedDeclaration > TSTypeAliasDeclaration', + 'ExportNamedDeclaration > TSEnumDeclaration', + ], + checkConstructors: false, + checkGetters: false, + checkSetters: false, + enableFixer: false, + }, + ], + 'jsdoc/require-description': [ + 'error', + { + contexts: ['any'], + exemptedBy: ['deprecated', 'inheritdoc', 'internal', 'see'], + }, + ], + 'jsdoc/require-param': 'off', + 'jsdoc/require-returns': 'off', + 'jsdoc/require-yields': 'off', + // Types belong in the TypeScript type annotations, not the JSDoc tags. + 'jsdoc/require-throws-type': 'off', + 'jsdoc/tag-lines': ['error', 'never', { startLines: 1 }], + // Conflicts with how Prettier formats JSDoc inside union types. + 'jsdoc/check-alignment': 'off', + }, + }, { files, plugins: { diff --git a/package-lock.json b/package-lock.json index c191c72f..37b5e02f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "eslint": "^9.31.0", "eslint-import-resolver-typescript": "^4.4.5", "eslint-plugin-import": "^2.32.0", + "eslint-plugin-jsdoc": "^64.1.0", "eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-unused-imports": "^4.1.4", "execa": "^10.0.0", @@ -101,6 +102,47 @@ "tslib": "^2.4.0" } }, + "node_modules/@es-joy/jsdoccomment": { + "version": "0.95.0", + "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.95.0.tgz", + "integrity": "sha512-jbzwtRPuw1Nzld0lacvr1vwzPU/6zEHFGK5YOTstl2MQYMZBuKmSW3HMuEwsq1v4meK5Do4lizxyd/hi25rCZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.9", + "@typescript-eslint/types": "^8.67.0", + "comment-parser": "1.4.8", + "esquery": "^1.7.0", + "jsdoc-type-pratt-parser": "~9.1.1" + }, + "engines": { + "node": "^22.22.2 || >=24.15.0" + } + }, + "node_modules/@es-joy/jsdoccomment/node_modules/@typescript-eslint/types": { + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.67.0.tgz", + "integrity": "sha512-sBtgslww8nsMYUjhdPBiSyUqSzT8uR6g93A2QXnQC8+cGdjz0CyaOdqHDRJb1AtORbZCNUJBBeFA/tNR2uQmww==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@es-joy/resolve.exports": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@es-joy/resolve.exports/-/resolve.exports-1.2.0.tgz", + "integrity": "sha512-Q9hjxWI5xBM+qW2enxfe8wDKdFWMfd0Z29k5ZJnuBqD/CasY5Zryj09aCA6owbGATWz+39p5uIdaHXpopOcG8g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.28.2", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", @@ -1170,6 +1212,19 @@ "dev": true, "license": "MIT" }, + "node_modules/@sindresorhus/base62": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/base62/-/base62-1.0.0.tgz", + "integrity": "sha512-TeheYy0ILzBEI/CO55CP6zJCSdSWeRtGnHy8U8dWSUH4I68iqTsy7HkMktR4xakThc9jotkPQUXT4ITdbV7cHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@sindresorhus/merge-streams": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", @@ -2075,6 +2130,16 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/are-docs-informative": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/are-docs-informative/-/are-docs-informative-0.1.1.tgz", + "integrity": "sha512-sqRsNQBwbKLRX0jV5Cu5uzmtflf892n4Vukz7T659ebL4pz3mpOqCMU7lxMoBTFwnp10E3YB5ZcyHM41W5bcDA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -2875,6 +2940,16 @@ "node": ">=16" } }, + "node_modules/comment-parser": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.8.tgz", + "integrity": "sha512-rKZTGo4fzKYna8UcL0isTg5wkBNla7bxTypLwZQXjIdi++IdP1OJ41rI5Mti3/jltkPujbu4i9LIARYA+zpotQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/common-path-prefix": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", @@ -3870,6 +3945,79 @@ "semver": "bin/semver.js" } }, + "node_modules/eslint-plugin-jsdoc": { + "version": "64.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-64.1.0.tgz", + "integrity": "sha512-UyAR/zVWn1jgIM/7eDMeLHuqLVtqKtamNVE0M7mdcZtFkWdYlNvDVDzyCCcURmLe66JpTZkMM3ZM3zjiVvIByA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@es-joy/jsdoccomment": "~0.95.0", + "@es-joy/resolve.exports": "1.2.0", + "are-docs-informative": "^0.1.1", + "comment-parser": "1.4.8", + "debug": "^4.4.3", + "escape-string-regexp": "^5.0.0", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "html-entities": "^2.6.0", + "object-deep-merge": "^2.0.1", + "parse-imports-exports": "^0.2.4", + "semver": "^7.8.5", + "spdx-expression-parse": "^5.0.0", + "to-valid-identifier": "^1.0.0" + }, + "engines": { + "node": "^22.22.2 || >=24.15.0" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-plugin-jsdoc/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-plugin-jsdoc/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-jsdoc/node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/eslint-plugin-n": { "version": "17.24.0", "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-17.24.0.tgz", @@ -5090,6 +5238,23 @@ "node": ">= 6" } }, + "node_modules/html-entities": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", + "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "license": "MIT" + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -5971,6 +6136,19 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdoc-type-pratt-parser": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-9.1.1.tgz", + "integrity": "sha512-kojSbQb9iQM2bk2PmHiKa3reG0U4v2gN9U8D8+eCQq+4KM5ZXBUyBVeF8KmR0RiwqyrW4+uVWYWTOLnpsavnkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.9" + }, + "engines": { + "node": "^22.22.2 || >=24.15.0" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -7080,6 +7258,13 @@ "node": ">=0.10.0" } }, + "node_modules/object-deep-merge": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/object-deep-merge/-/object-deep-merge-2.0.1.tgz", + "integrity": "sha512-aKttDKcU3pyZqKcCkDhsMn70WmZFG2JGDQLP9EcLyTSIFQRCPWLAmBZRLJnrVUrhPG1jETEEbfdgbNtJf1LyMg==", + "dev": true, + "license": "MIT" + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -7342,6 +7527,16 @@ "node": ">=6" } }, + "node_modules/parse-imports-exports": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/parse-imports-exports/-/parse-imports-exports-0.2.4.tgz", + "integrity": "sha512-4s6vd6dx1AotCx/RCI2m7t7GCh5bDRUtGNvRfHSP2wbBQdMi67pPe7mtzmgwcaQ8VKK/6IB7Glfyu3qdZJPybQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse-statements": "1.0.11" + } + }, "node_modules/parse-ms": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", @@ -7355,6 +7550,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse-statements": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/parse-statements/-/parse-statements-1.0.11.tgz", + "integrity": "sha512-HlsyYdMBnbPQ9Jr/VgJ1YF4scnldvJpJxCVx6KgqPL4dxppsWrJHCIIxQXMJrqGnsRkNPATbeMJ8Yxu7JMsYcA==", + "dev": true, + "license": "MIT" + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -7848,6 +8050,19 @@ "license": "MIT", "peer": true }, + "node_modules/reserved-identifiers": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/reserved-identifiers/-/reserved-identifiers-1.2.0.tgz", + "integrity": "sha512-yE7KUfFvaBFzGPs5H3Ops1RevfUEsDc5Iz65rOwWg4lE8HJSYtle77uul3+573457oHvBKuHYDl/xqUkKpEEdw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/resolve": { "version": "2.0.0-next.7", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz", @@ -8289,6 +8504,31 @@ "node": ">=0.10.0" } }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-5.0.0.tgz", + "integrity": "sha512-vngmw3Rgn+o2arXNbnZaj5UtOEBuWBfvaI+Wc8GFfykIhA5/vdK9/Sp/XkLv63dykz2rxKDvKEHupF5P0FORcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", + "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -8771,6 +9011,23 @@ "node": ">=8.0" } }, + "node_modules/to-valid-identifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/to-valid-identifier/-/to-valid-identifier-1.0.0.tgz", + "integrity": "sha512-41wJyvKep3yT2tyPqX/4blcfybknGB4D+oETKLs7Q76UiPqRpUJK3hr1nxelyYO0PHKVzJwlu0aCeEAsGI6rpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/base62": "^1.0.0", + "reserved-identifiers": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", diff --git a/package.json b/package.json index 854cfd84..8a3d8cd9 100644 --- a/package.json +++ b/package.json @@ -97,6 +97,7 @@ "eslint": "^9.31.0", "eslint-import-resolver-typescript": "^4.4.5", "eslint-plugin-import": "^2.32.0", + "eslint-plugin-jsdoc": "^64.1.0", "eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-unused-imports": "^4.1.4", "execa": "^10.0.0", diff --git a/src/index.ts b/src/index.ts index 4eb60931..3bb9f91f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1 +1,6 @@ +/** + * JavaScript HTTP client for the Seam API written in TypeScript. + * + * @packageDocumentation + */ export * from './lib/index.js' diff --git a/src/lib/client.ts b/src/lib/client.ts index 383e2cdd..c702ba59 100644 --- a/src/lib/client.ts +++ b/src/lib/client.ts @@ -13,8 +13,22 @@ export type Client = AxiosInstance export const defaultTimeout = 30_000 export interface ClientOptions { + /** + * Request timeout in milliseconds, applied per attempt. + * Set to 0 to disable the timeout. Defaults to 30 seconds. + */ timeout?: number + + /** + * Options passed to the underlying Axios client. + */ axiosOptions?: AxiosRequestConfig + + /** + * Options passed to axios-retry, + * which retries idempotent requests that fail + * because of a transport error, timeout, or HTTP 429 response. + */ axiosRetryOptions?: AxiosRetryConfig } diff --git a/src/lib/index.ts b/src/lib/index.ts index edc7b05a..433f021b 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -3,12 +3,15 @@ export * from './error-interceptor.js' export * from './openapi.js' export * from './options.js' export { + type FailedActionAttempt, isSeamActionAttemptError, isSeamActionAttemptFailedError, isSeamActionAttemptTimeoutError, + type ResolveActionAttemptOptions, SeamActionAttemptError, SeamActionAttemptFailedError, SeamActionAttemptTimeoutError, + type SucceededActionAttempt, } from './resolve-action-attempt.js' export * from './resources/index.js' export * from './routes/index.js' diff --git a/src/lib/options.ts b/src/lib/options.ts index a91807f9..4b2984cd 100644 --- a/src/lib/options.ts +++ b/src/lib/options.ts @@ -6,12 +6,19 @@ import { export type { SeamHttpRequestOptions } from './request-options.js' +/** + * Options for creating a SeamHttpWithoutWorkspace client, + * which is not scoped to a single workspace. + */ export type SeamHttpWithoutWorkspaceOptions = | SeamHttpWithoutWorkspaceOptionsFromEnv | SeamHttpWithoutWorkspaceOptionsWithClient | SeamHttpWithoutWorkspaceOptionsWithConsoleSessionToken | SeamHttpWithoutWorkspaceOptionsWithPersonalAccessToken +/** + * Options for creating a SeamHttp client. + */ export type SeamHttpOptions = | SeamHttpOptionsFromEnv | SeamHttpOptionsWithClient @@ -21,28 +28,62 @@ export type SeamHttpOptions = | SeamHttpOptionsWithPersonalAccessToken interface SeamHttpCommonOptions extends ClientOptions, SeamHttpRequestOptions { + /** + * The Seam API endpoint, e.g., `https://connect.getseam.com`. + * Defaults to the SEAM_ENDPOINT or SEAM_API_URL environment variable, if set. + */ endpoint?: string } +/** + * Options for creating a SeamHttp client with `SeamHttp.fromPublishableKey`. + */ export interface SeamHttpFromPublishableKeyOptions extends SeamHttpCommonOptions {} +/** + * Options for creating a SeamHttp client that reads its configuration + * from the environment, e.g., the SEAM_API_KEY environment variable. + */ export interface SeamHttpOptionsFromEnv extends SeamHttpCommonOptions {} +/** + * Options for creating a SeamHttpWithoutWorkspace client that reads + * its configuration from the environment, + * e.g., the SEAM_PERSONAL_ACCESS_TOKEN environment variable. + */ export interface SeamHttpWithoutWorkspaceOptionsFromEnv extends SeamHttpCommonOptions {} +/** + * Options for creating a SeamHttpWithoutWorkspace client from an existing client. + */ export interface SeamHttpWithoutWorkspaceOptionsWithClient extends SeamHttpCommonOptions { client: Client } +/** + * Returns true if the options include a client. + * + * @throws {@link SeamHttpInvalidOptionsError} if the client option + * is used with any other option. + */ export const isSeamHttpWithoutWorkspaceOptionsWithClient = ( options: SeamHttpOptions, ): options is SeamHttpWithoutWorkspaceOptionsWithClient => isSeamHttpOptionsWithClient(options) +/** + * Options for creating a SeamHttp client from an existing client. + */ export interface SeamHttpOptionsWithClient extends SeamHttpRequestOptions { client: Client } +/** + * Returns true if the options include a client. + * + * @throws {@link SeamHttpInvalidOptionsError} if the client option + * is used with any other option. + */ export const isSeamHttpOptionsWithClient = ( options: SeamHttpOptions, ): options is SeamHttpOptionsWithClient => { @@ -61,10 +102,19 @@ export const isSeamHttpOptionsWithClient = ( return true } +/** + * Options for creating a SeamHttp client authenticated with an API key. + */ export interface SeamHttpOptionsWithApiKey extends SeamHttpCommonOptions { apiKey: string } +/** + * Returns true if the options include an apiKey. + * + * @throws {@link SeamHttpInvalidOptionsError} if the apiKey option + * is used with another authentication option. + */ export const isSeamHttpOptionsWithApiKey = ( options: SeamHttpOptions, ): options is SeamHttpOptionsWithApiKey => { @@ -92,10 +142,19 @@ export const isSeamHttpOptionsWithApiKey = ( return true } +/** + * Options for creating a SeamHttp client authenticated with a client session token. + */ export interface SeamHttpOptionsWithClientSessionToken extends SeamHttpCommonOptions { clientSessionToken: string } +/** + * Returns true if the options include a clientSessionToken. + * + * @throws {@link SeamHttpInvalidOptionsError} if the clientSessionToken option + * is used with another authentication option. + */ export const isSeamHttpOptionsWithClientSessionToken = ( options: SeamHttpOptions, ): options is SeamHttpOptionsWithClientSessionToken => { @@ -123,10 +182,20 @@ export const isSeamHttpOptionsWithClientSessionToken = ( return true } +/** + * Options for creating a SeamHttpWithoutWorkspace client + * authenticated with a console session token. + */ export interface SeamHttpWithoutWorkspaceOptionsWithConsoleSessionToken extends SeamHttpCommonOptions { consoleSessionToken: string } +/** + * Returns true if the options include a consoleSessionToken. + * + * @throws {@link SeamHttpInvalidOptionsError} if the consoleSessionToken option + * is used with another authentication option. + */ export const isSeamHttpWithoutWorkspaceOptionsWithConsoleSessionToken = ( options: SeamHttpOptions, ): options is SeamHttpWithoutWorkspaceOptionsWithConsoleSessionToken => { @@ -154,11 +223,21 @@ export const isSeamHttpWithoutWorkspaceOptionsWithConsoleSessionToken = ( return true } +/** + * Options for creating a SeamHttp client + * authenticated with a console session token and scoped to a workspace. + */ export interface SeamHttpOptionsWithConsoleSessionToken extends SeamHttpCommonOptions { consoleSessionToken: string workspaceId: string } +/** + * Returns true if the options include a consoleSessionToken and a workspaceId. + * + * @throws {@link SeamHttpInvalidOptionsError} if the consoleSessionToken option + * is used with another authentication option or without the workspaceId option. + */ export const isSeamHttpOptionsWithConsoleSessionToken = ( options: SeamHttpOptions, ): options is SeamHttpOptionsWithConsoleSessionToken => { @@ -175,10 +254,20 @@ export const isSeamHttpOptionsWithConsoleSessionToken = ( return true } +/** + * Options for creating a SeamHttpWithoutWorkspace client + * authenticated with a personal access token. + */ export interface SeamHttpWithoutWorkspaceOptionsWithPersonalAccessToken extends SeamHttpCommonOptions { personalAccessToken: string } +/** + * Returns true if the options include a personalAccessToken. + * + * @throws {@link SeamHttpInvalidOptionsError} if the personalAccessToken option + * is used with another authentication option. + */ export const isSeamHttpWithoutWorkspaceOptionsWithPersonalAccessToken = ( options: SeamHttpOptions, ): options is SeamHttpWithoutWorkspaceOptionsWithPersonalAccessToken => { @@ -206,11 +295,21 @@ export const isSeamHttpWithoutWorkspaceOptionsWithPersonalAccessToken = ( return true } +/** + * Options for creating a SeamHttp client + * authenticated with a personal access token and scoped to a workspace. + */ export interface SeamHttpOptionsWithPersonalAccessToken extends SeamHttpCommonOptions { personalAccessToken: string workspaceId: string } +/** + * Returns true if the options include a personalAccessToken and a workspaceId. + * + * @throws {@link SeamHttpInvalidOptionsError} if the personalAccessToken option + * is used with another authentication option or without the workspaceId option. + */ export const isSeamHttpOptionsWithPersonalAccessToken = ( options: SeamHttpOptions, ): options is SeamHttpOptionsWithPersonalAccessToken => { @@ -227,6 +326,9 @@ export const isSeamHttpOptionsWithPersonalAccessToken = ( return true } +/** + * Error thrown when a SeamHttp client is created with invalid options. + */ export class SeamHttpInvalidOptionsError extends Error { constructor(message: string) { super(`SeamHttp received invalid options: ${message}`) @@ -234,6 +336,9 @@ export class SeamHttpInvalidOptionsError extends Error { } } +/** + * Error thrown when a SeamHttpWithoutWorkspace client is created with invalid options. + */ export class SeamHttpWithoutWorkspaceInvalidOptionsError extends Error { constructor(message: string) { super(`SeamHttpWithoutWorkspace received invalid options: ${message}`) diff --git a/src/lib/request-options.ts b/src/lib/request-options.ts index e403a4f7..0fad41d8 100644 --- a/src/lib/request-options.ts +++ b/src/lib/request-options.ts @@ -1,9 +1,22 @@ import type { ResolveActionAttemptOptions } from './resolve-action-attempt.js' +/** + * Options for how a {@link SeamHttpRequest} is executed. + */ export interface SeamHttpRequestOptions { + /** + * Controls whether to wait for the action attempt to resolve + * when a request returns an action attempt. + * Pass a boolean to toggle waiting, + * or {@link ResolveActionAttemptOptions} to also configure + * the timeout and polling interval. + */ waitForActionAttempt?: boolean | ResolveActionAttemptOptions } +/** + * Returns true if the key is the name of a {@link SeamHttpRequestOptions} property. + */ export const isSeamHttpRequestOption = ( key: string, ): key is keyof SeamHttpRequestOptions => { diff --git a/src/lib/resolve-action-attempt.ts b/src/lib/resolve-action-attempt.ts index 3064d511..e01607be 100644 --- a/src/lib/resolve-action-attempt.ts +++ b/src/lib/resolve-action-attempt.ts @@ -1,14 +1,35 @@ import type { ActionAttempt } from './resources/action-attempt.js' +/** + * Client used to poll action attempts by id, e.g., `SeamHttpActionAttempts`. + */ export interface ActionAttemptsClient { get(parameters: { action_attempt_id: string }): PromiseLike } +/** + * Options for waiting until an action attempt resolves. + */ export interface ResolveActionAttemptOptions { + /** + * Maximum time in milliseconds to wait for the action attempt to resolve. + */ timeout?: number + + /** + * Time in milliseconds to wait between polls of the action attempt. + */ pollingInterval?: number } +/** + * Polls a pending action attempt until it succeeds, fails, or times out. + * + * @returns The succeeded action attempt. + * @throws {@link SeamActionAttemptFailedError} if the action attempt fails. + * @throws {@link SeamActionAttemptTimeoutError} if the action attempt + * does not resolve within the timeout. + */ export const resolveActionAttempt = async ( actionAttempt: T, actionAttempts: ActionAttemptsClient, @@ -59,12 +80,18 @@ const pollActionAttempt = async ( ) } +/** + * Returns true if the error is a {@link SeamActionAttemptError}. + */ export const isSeamActionAttemptError = ( error: unknown, ): error is SeamActionAttemptError => { return error instanceof SeamActionAttemptError } +/** + * Error relating to an action attempt. + */ export class SeamActionAttemptError extends Error { actionAttempt: T @@ -75,15 +102,24 @@ export class SeamActionAttemptError extends Error { } } +/** + * Returns true if the error is a {@link SeamActionAttemptFailedError}. + */ export const isSeamActionAttemptFailedError = ( error: unknown, ): error is SeamActionAttemptFailedError => { return error instanceof SeamActionAttemptFailedError } +/** + * Error thrown when an action attempt fails. + */ export class SeamActionAttemptFailedError< T extends ActionAttempt, > extends SeamActionAttemptError { + /** + * Error type returned by the Seam API for the failed action attempt. + */ code: string constructor(actionAttempt: FailedActionAttempt) { @@ -93,12 +129,18 @@ export class SeamActionAttemptFailedError< } } +/** + * Returns true if the error is a {@link SeamActionAttemptTimeoutError}. + */ export const isSeamActionAttemptTimeoutError = ( error: unknown, ): error is SeamActionAttemptTimeoutError => { return error instanceof SeamActionAttemptTimeoutError } +/** + * Error thrown when an action attempt does not resolve within the timeout. + */ export class SeamActionAttemptTimeoutError< T extends ActionAttempt, > extends SeamActionAttemptError { @@ -120,10 +162,16 @@ const isFailedActionAttempt = ( actionAttempt: T, ): actionAttempt is FailedActionAttempt => actionAttempt.status === 'error' +/** + * An action attempt that has succeeded. + */ export type SucceededActionAttempt = T & { status: 'success' } +/** + * An action attempt that has failed. + */ export type FailedActionAttempt = T & { status: 'error' } diff --git a/src/lib/seam-http-error.ts b/src/lib/seam-http-error.ts index 7e8dfe04..e16f3c2e 100644 --- a/src/lib/seam-http-error.ts +++ b/src/lib/seam-http-error.ts @@ -1,9 +1,25 @@ import type { ApiError } from './api-error-types.js' +/** + * Error thrown when the Seam API returns an error response. + */ export class SeamHttpApiError extends Error { + /** + * Error type returned by the Seam API, e.g., `invalid_input`. + */ code: string + statusCode: number + + /** + * Unique identifier of the request that failed. + * Provide this to Seam support when reporting an issue. + */ requestId: string + + /** + * Additional error-specific data returned by the Seam API, if any. + */ data?: unknown constructor(error: ApiError, statusCode: number, requestId: string) { @@ -17,12 +33,18 @@ export class SeamHttpApiError extends Error { } } +/** + * Returns true if the error is a {@link SeamHttpApiError}. + */ export const isSeamHttpApiError = ( error: unknown, ): error is SeamHttpApiError => { return error instanceof SeamHttpApiError } +/** + * Error thrown when the Seam API returns a 401 Unauthorized error response. + */ export class SeamHttpUnauthorizedError extends SeamHttpApiError { override code: 'unauthorized' override statusCode: 401 @@ -38,14 +60,21 @@ export class SeamHttpUnauthorizedError extends SeamHttpApiError { } } +/** + * Returns true if the error is a {@link SeamHttpUnauthorizedError}. + */ export const isSeamHttpUnauthorizedError = ( error: unknown, ): error is SeamHttpUnauthorizedError => { return error instanceof SeamHttpUnauthorizedError } +/** + * Error thrown when the Seam API returns an `invalid_input` error response. + */ export class SeamHttpInvalidInputError extends SeamHttpApiError { override code: 'invalid_input' + readonly #validationErrors: NonNullable constructor(error: ApiError, statusCode: number, requestId: string) { @@ -55,11 +84,20 @@ export class SeamHttpInvalidInputError extends SeamHttpApiError { this.#validationErrors = error.validation_errors ?? {} } + /** + * Returns the validation error messages for the request parameter, + * or an empty array if the parameter had no validation errors. + * + * @param paramName - Name of the request parameter. + */ getValidationErrorMessages(paramName: string): string[] { return this.#validationErrors[paramName]?._errors ?? [] } } +/** + * Returns true if the error is a {@link SeamHttpInvalidInputError}. + */ export const isSeamHttpInvalidInputError = ( error: unknown, ): error is SeamHttpInvalidInputError => { diff --git a/src/lib/seam-http-request.ts b/src/lib/seam-http-request.ts index c391551e..719de08d 100644 --- a/src/lib/seam-http-request.ts +++ b/src/lib/seam-http-request.ts @@ -29,6 +29,20 @@ interface SeamHttpRequestConfig { readonly requiredParameterNames?: readonly string[] } +/** + * A lazy request to the Seam API. + * + * Creating a SeamHttpRequest does not send anything over the network. + * The request is sent once `execute` is called, + * or when the request is awaited like a Promise, + * e.g., with `await`, `then`, `catch`, or `finally`. + * When the response contains an action attempt, + * awaiting the request also waits for the action attempt to resolve + * according to the `waitForActionAttempt` option. + * + * Before sending, the request may be inspected + * with `url`, `pathname`, `method`, `params`, and `body`. + */ export class SeamHttpRequest< const TResponse, const TResponseKey extends keyof TResponse | undefined, @@ -48,6 +62,10 @@ export class SeamHttpRequest< this.#config = config } + /** + * The key of the API response object containing the response data, + * or undefined if the endpoint returns an empty response. + */ public get responseKey(): TResponseKey { return this.#config.responseKey } @@ -56,6 +74,9 @@ export class SeamHttpRequest< return this.#config.hasPagination ?? false } + /** + * The full request URL including any serialized query parameters. + */ public get url(): URL { const { client } = this.#parent @@ -92,6 +113,12 @@ export class SeamHttpRequest< return this.#config.body } + /** + * Sends the request and returns the response data. + * If the response contains an action attempt, + * waits for the action attempt to resolve + * according to the `waitForActionAttempt` option. + */ async execute(): Promise< TResponseKey extends keyof TResponse ? TResponse[TResponseKey] : undefined > { @@ -130,6 +157,10 @@ export class SeamHttpRequest< return data } + /** + * Sends the request and returns the entire response body + * without waiting for any action attempt to resolve. + */ async fetchResponse(): Promise { assertValidRequestParameters( this.#config.parameters, diff --git a/src/lib/seam-paginator.ts b/src/lib/seam-paginator.ts index 5fed0e9c..0741f46f 100644 --- a/src/lib/seam-paginator.ts +++ b/src/lib/seam-paginator.ts @@ -9,6 +9,9 @@ interface SeamPaginatorParent { declare const $brand: unique symbol +/** + * Opaque cursor identifying a page of results returned by the Seam API. + */ export type SeamPageCursor = string & { [$brand]: 'SeamPageCursor' } interface Pagination { @@ -17,6 +20,15 @@ interface Pagination { readonly nextPageUrl: string | null } +/** + * Iterates over paginated results from Seam API list endpoints. + * + * Create a SeamPaginator with the client's `createPaginator` method. + * Fetch pages manually with `firstPage` and `nextPage`, + * iterate over pages with `for await`, + * iterate over items across all pages with `flatten`, + * or collect every item into a single array with `flattenToArray`. + */ export class SeamPaginator< const TResponse, const TResponseKey extends keyof TResponse, @@ -37,12 +49,19 @@ export class SeamPaginator< this.#request = request } + /** + * Fetches the first page of results along with the pagination state. + */ async firstPage(): Promise< [EnsureReadonlyArray, Pagination] > { return await this.#fetch() } + /** + * Fetches the next page of results + * using the nextPageCursor returned with a previous page. + */ async nextPage( nextPageCursor: Pagination['nextPageCursor'], ): Promise<[EnsureReadonlyArray, Pagination]> { @@ -104,6 +123,9 @@ export class SeamPaginator< ] as const } + /** + * Fetches every page and returns all items in a single array. + */ async flattenToArray(): Promise< EnsureReadonlyArray > { @@ -117,6 +139,9 @@ export class SeamPaginator< return items as EnsureReadonlyArray } + /** + * Yields each item across all pages, fetching the next page as needed. + */ async *flatten(): AsyncGenerator< EnsureReadonlyArray > { @@ -132,6 +157,9 @@ export class SeamPaginator< } } + /** + * Yields each page of items, fetching the next page as needed. + */ async *[Symbol.asyncIterator](): AsyncGenerator< EnsureReadonlyArray > {