diff --git a/auth.ts b/auth.ts index 44106d3..c6388dc 100644 --- a/auth.ts +++ b/auth.ts @@ -1,5 +1,8 @@ export const SPOTIFY_AUTH_URL = "https://accounts.spotify.com/"; +/** + * @see https://developer.spotify.com/documentation/web-api/concepts/scopes + */ export const OAUTH_SCOPES = { /** * Write access to user-provided images. diff --git a/client.ts b/client.ts index 3d9d42b..2cc6631 100644 --- a/client.ts +++ b/client.ts @@ -52,8 +52,6 @@ export class SpotifyError extends Error { } } -const APP_JSON = "application/json"; - const getBodyMessage = ( body: RegularErrorObject | string, ): string => { @@ -109,11 +107,11 @@ interface MiddlewareOptions extends Omit { headers: Headers; } -type MiddlewareHandler = ( +type NextMiddleware = ( url: URL, options: MiddlewareOptions, ) => Promise; -export type Middleware = (next: MiddlewareHandler) => MiddlewareHandler; +export type Middleware = (next: NextMiddleware) => NextMiddleware; /** * Interface for making HTTP requests to the Spotify API. @@ -142,12 +140,19 @@ export interface SpotifyClinetOptions { */ baseUrl?: string; /** - * @returns new access token + * Function that will be called when the access token is expired. + * @returns New access token. */ refresher?: () => Promise; /** * Weather to wait for rate limit or not. \ * Function can be used to decide dynamically based on the `retryAfter` time in seconds. + * ```ts + * { + * waitForRateLimit: (retryAfter) => retryAfter < 10, + * } + * ``` + * **Be aware that this can cause a long delay in the response.** * * @default false */ @@ -175,6 +180,37 @@ const createFailedToAuthorizeError = () => "[SpotifyClient] accessToken or refresher is required to make requests.", ); +const APP_JSON = "application/json"; + +/** + * The main class that provides fetch method for making requests with build-in functionality for token refreshing, error handling and more. + * + * @example + * ```ts + * import { SpotifyClient } from "@soundify/web-api"; + * + * // with access token + * const client = new SpotifyClient("YOUR_ACCESS_TOKEN"); + * + * // with automatic token refreshing + * const client = new SpotifyClient(null, { + * // your custom refresher here + * refresher: () => Promise.resolve("NEW_ACCESS_TOKEN"), + * }); + * + * // with custom options + * const client = new SpotifyClient("YOUR_ACCESS_TOKEN", { + * waitForRateLimit: true, + * middlewares: [(next) => (url, options) => { + * options.headers.set("X-Custom-Header", "custom-value"); + * return next(url, options); + * }], + * }) + * + * const res = await client.fetch("/v1/me"); + * const user = await res.json(); + * ``` + */ export class SpotifyClient implements HTTPClient { private readonly baseUrl: string; private refreshInProgress: Promise | null = null; @@ -203,7 +239,7 @@ export class SpotifyClient implements HTTPClient { if (opts.query) { for (const key in opts.query) { const value = opts.query[key]; - if (typeof value !== "undefined") { + if (value !== undefined) { url.searchParams.set(key, value.toString()); } } @@ -223,7 +259,7 @@ export class SpotifyClient implements HTTPClient { const wrappedFetch = (this.options.middlewares || []).reduceRight( (next, mw) => mw(next), - (this.options.fetch || globalThis.fetch) as MiddlewareHandler, + (this.options.fetch || globalThis.fetch) as NextMiddleware, ); let isRefreshed = false; diff --git a/endpoints/category/category.types.ts b/endpoints/category/category.types.ts index 6ad4b8a..49824c9 100644 --- a/endpoints/category/category.types.ts +++ b/endpoints/category/category.types.ts @@ -1,4 +1,3 @@ -import type { NonNullableObject } from "../../shared.ts"; import type { Image } from "../general.types.ts"; export interface Category { @@ -9,7 +8,7 @@ export interface Category { /** * The category icon, in various sizes. */ - icons: NonNullableObject[]; + icons: Image[]; /** * The Spotify category ID of the category. */ diff --git a/endpoints/general.types.ts b/endpoints/general.types.ts index 7a865ea..c8dcf93 100644 --- a/endpoints/general.types.ts +++ b/endpoints/general.types.ts @@ -81,14 +81,14 @@ export type PagingOptions = { */ export type RestrictionsReason = "market" | "product" | "explicit"; -export type Restrictions = { +export interface Restrictions { /** * The reason for the restriction. * * Episodes may be restricted if the content is not available in a given market, to the user's subscription type, or when the user's account is set to not play explicit content. */ reason: RestrictionsReason; -}; +} /** * The precision with which `release_date` value is known. @@ -110,7 +110,7 @@ export interface Image { width: number | null; } -export type ResumePoint = { +export interface ResumePoint { /** * Whether or not the episode has been fully played by the user. */ @@ -119,9 +119,9 @@ export type ResumePoint = { * The user's most recent position in the episode in milliseconds */ resume_position_ms: number; -}; +} -export type Followers = { +export interface Followers { /** * This will always be set to null, as the Web API does not support it at the moment. */ @@ -130,25 +130,25 @@ export type Followers = { * The total number of followers. */ total: number; -}; +} -export type Author = { +export interface Author { /** * The name of the author. */ name: string; -}; +} -export type Narrator = { +export interface Narrator { /** * The name of the narrator. */ name: string; -}; +} -export type ExternalUrls = { +export interface ExternalUrls { spotify: string; -}; +} export interface ExternalIds { /** @@ -168,7 +168,7 @@ export interface ExternalIds { /** * The copyright object contains the type and the name of copyright. */ -export type Copyright = { +export interface Copyright { /** * The copyright text for this content. */ @@ -179,4 +179,4 @@ export type Copyright = { * P = the sound recording (performance) copyright */ type: "C" | "P"; -}; +} diff --git a/endpoints/mod.ts b/endpoints/mod.ts index 0171e67..8309b2b 100644 --- a/endpoints/mod.ts +++ b/endpoints/mod.ts @@ -1,3 +1,9 @@ +/** + * @module + * This module contains all the endpoints and types for the Spotify Web API. + * + * @see https://developer.spotify.com/documentation/web-api + */ export * from "./user/user.types.ts"; export * from "./user/user.endpoints.ts"; export * from "./track/track.types.ts"; diff --git a/endpoints/playlist/playlist.endpoints.ts b/endpoints/playlist/playlist.endpoints.ts index ff1ebce..c658bfb 100644 --- a/endpoints/playlist/playlist.endpoints.ts +++ b/endpoints/playlist/playlist.endpoints.ts @@ -1,4 +1,4 @@ -import type { NonNullableObject, Prettify } from "../../shared.ts"; +import type { Prettify } from "../../shared.ts"; import type { Image, PagingObject, PagingOptions } from "../general.types.ts"; import type { FeaturedPlaylists, @@ -397,9 +397,9 @@ export const getCategoryPlaylists = async ( export const getPlaylistCoverImage = async ( client: HTTPClient, playlistId: string, -): Promise[]> => { +): Promise => { const res = await client.fetch(`/v1/playlists/${playlistId}/images`); - return res.json() as Promise[]>; + return res.json() as Promise; }; /** diff --git a/mod.ts b/mod.ts index 600af49..a5b0de2 100644 --- a/mod.ts +++ b/mod.ts @@ -1,3 +1,46 @@ +/** + * @module + * HTTP Client library that provides a simple interface to the Spotify Web API. + * + * ## SpotifyClient + * The main class that provides fetch method for making requests with build-in functionality for token refreshing, error handling and more. + * + * @example + * ```ts + * import { SpotifyClient } from "@soundify/web-api"; + * + * // with access token + * const client = new SpotifyClient("YOUR_ACCESS_TOKEN"); + * + * // with automatic token refreshing + * const client = new SpotifyClient(null, { + * // your custom refresher here + * refresher: () => Promise.resolve("NEW_ACCESS_TOKEN"), + * }); + * + * const res = await client.fetch("/v1/me"); + * const user = await res.json(); + * ``` + * + * ## Endpoints + * Functions that utilize the `SpotifyClient` to make requests to the Spotify Web API. + * + * @example + * ``` + * import { SpotifyClient, getCurrentUser } from "@soundify/web-api"; + * + * const client = new SpotifyClient("YOUR_ACCESS_TOKEN"); + * const user = await getCurrentUser(client); + * + * // How endpoint functions are built + * const getCurrentUser = async (client: SpotifyClient) => { + * const res = await client.fetch("/v1/me"); + * return await res.json(); + * } + * ``` + * + * @see https://developer.spotify.com/documentation/web-api + */ export { type FetchLikeOptions, type HTTPClient, diff --git a/pagination.ts b/pagination.ts index 452c93a..911be4a 100644 --- a/pagination.ts +++ b/pagination.ts @@ -1,4 +1,4 @@ -export type PageIteratorOptions = { +export interface PageIteratorOptions { /** * The Spotify API does not allow you to use a negative offset, but you can do so with this property. This will be useful when, for example, you want to get the last 100 elements. * @@ -7,7 +7,7 @@ export type PageIteratorOptions = { * @default 0 */ initialOffset?: number; -}; +} /** * A helper class which allows you to iterate over items in a paginated API response with javascript async iterators. diff --git a/shared.ts b/shared.ts index 50797b4..20cc8cb 100644 --- a/shared.ts +++ b/shared.ts @@ -1,7 +1,3 @@ -export type NonNullableObject = { - [K in keyof T]: NonNullable; -}; - export type RequireAtLeastOne = { [K in keyof T]-?: & Required>