Home Reference Source Test Repository

src/index.js

import 'isomorphic-fetch';
import Action from './model/action';
import Logger from './util/logger';
import Authorization from './controller/authorization';
import Listening from './controller/listening';
import Identity from './controller/identity';
import StationFinder from './controller/station-finder';

Logger.setLevel(Logger.WARN);

/**
 * This SDK attempts to abstract away most of the interaction with the NPR One API.
 * In general, a consumer of this API should be primarily concerned with asking for
 * recommendations and recording user actions against those recommendations.
 */
export class NprOneSDK {
    /**
     * Instantiates the NPR One SDK.
     */
    constructor() {
        /** @type {null|Function} A callback that gets triggered whenever the access token has changed
         * @private */
        this._accessTokenChangedCallback = null;
        /** @type {Authorization}
         * @private */
        this._authorization = new Authorization();
        /** @type {Listening}
         * @private */
        this._listening = new Listening();
        /** @type {Identity}
         * @private */
        this._identity = new Identity();
        /** @type {StationFinder}
         * @private */
        this._stationfinder = new StationFinder();

        // setup the default config
        NprOneSDK._initConfig();
    }

    /**
     * @typedef {Object} Config
     * @property {string} [apiBaseUrl='https://api.npr.org'] DEPRECATED / NO LONGER IN USE: The NPR One API hostname and protocol, typically `https://api.npr.org`; in most cases, this does not need to be manually set by clients
     * @property {string} [apiVersion='v2'] DEPRECATED / NO LONGER IN USE: The NPR One API version, typically `v2`; in most cases, this does not need to be manually set by clients
     * @property {string} [authProxyBaseUrl] The full URL to your OAuth proxy, e.g. `https://one.example.com/oauth2/`
     * @property {string} [newDeviceCodePath='/device'] The path to your proxy for starting a `device_code` grant (relative to `authProxyBaseUrl`)
     * @property {string} [pollDeviceCodePath='/device/poll'] The path to your proxy for polling a `device_code` grant (relative to `authProxyBaseUrl`)
     * @property {string} [refreshTokenPath='/refresh'] The path to your proxy for the `refresh_token` grant (relative to `authProxyBaseUrl`)
     * @property {string} [tempUserPath='/temporary'] The path to your proxy for the `temporary_user` grant (relative to `authProxyBaseUrl`), not available to third-party clients
     * @property {string} [logoutPath='/logout'] The path to your proxy for the `POST /authorization/v2/token/revoke` endpoint (relative to `authProxyBaseUrl`)
     * @property {string} [accessToken] The access token to use if not using the auth proxy
     * @property {string} [clientId] The NPR One API `client_id` to use, only required if using the auth proxy with the `temporary_user` grant type
     * @property {string} [advertisingId] The custom X-Advertising-ID header to send with most requests, not typically used by third-party clients
     * @property {string} [advertisingTarget] The custom X-Advertising-Target header to send with most requests, not typically used by third-party clients
     * @property {string} [subdomain] The custom subdomain to use for requests, not typically used by third-party clients
     */
    /**
     * @type {Config}
     */
    static get config() {
        NprOneSDK._initConfig();
        return NprOneSDK._config;
    }

    /**
     * Updates private `_config` member attributes but does not overwrite entire `_config` object
     *
     * @type {Config}
     */
    static set config(value) {
        if (value.apiBaseUrl) {
            Logger.warn('Property "apiBaseUrl" in config is deprecated '
                + 'and will be removed in a future release. '
                + 'Please use the "subdomain" property instead if a different API URL is needed.');
        }
        if (value.apiVersion) {
            Logger.warn('Property "apiVersion" in config is deprecated '
                + 'and will be removed in a future release.');
        }

        NprOneSDK._initConfig();
        Object.assign(NprOneSDK._config, value);
    }

    /** @type {string} */
    static get accessToken() {
        return NprOneSDK.config.accessToken;
    }

    /** @type {string} */
    static set accessToken(token) {
        if (typeof token !== 'string') {
            throw new TypeError('Value for accessToken must be a string');
        }

        const oldToken = NprOneSDK.accessToken;
        NprOneSDK.config.accessToken = token;

        if (oldToken !== token && typeof NprOneSDK._accessTokenChangedCallback === 'function') {
            NprOneSDK._accessTokenChangedCallback(token);
        }
    }

    /**
     * Sets a callback to be triggered whenever the SDK rotates the access token for a new one, usually when
     * the old token expires and a `refresh_token` is used to generate a fresh token. Clients who wish to persist
     * access tokens across sessions are urged to use this callback to be notified whenever a token change has
     * occurred; the only other alternative is to call `get accessToken()` after every API call.
     *
     * @type {Function}
     * @throws {TypeError} if the passed-in value isn't a function
     */
    static set onAccessTokenChanged(callback) {
        if (typeof callback !== 'function') {
            throw new TypeError('Value for onAccessTokenChanged must be a function');
        }
        NprOneSDK._accessTokenChangedCallback = callback;
    }

    /**
     * Exposes the Action class for clients to record actions
     *
     * @type {Action}
     */
    static get Action() {
        return Action;
    }

    /**
     * Exposes the Logger class for clients to adjust logging if desired
     *
     * @type {src/util/logger.js~Logger}
     */
    static get Logger() {
        return Logger;
    }

    /* Authorization */

    /**
     * See {@link Authorization.refreshExistingAccessToken} for description.
     *
     * @param {number} [numRetries=0]   The number of times this function has been tried. Will retry up to 3 times.
     * @returns {Promise<AccessToken>}
     * @throws {TypeError} if an OAuth proxy is not configured or no access token is set
     */
    static refreshExistingAccessToken(numRetries = 0) {
        return Authorization.refreshExistingAccessToken(numRetries);
    }

    /**
     * See {@link Authorization#logout} for description.
     *
     * @returns {Promise}
     * @throws {TypeError} if an OAuth proxy is not configured or no access token is currently set
     */
    logout() {
        return this._authorization.logout();
    }

    /**
     * See {@link Authorization#getDeviceCode} for description.
     *
     * @param {Array<string>} [scopes=[]]   The scopes (as strings) that should be associated with the resulting access token
     * @returns {Promise<DeviceCode>}
     * @throws {TypeError} if an OAuth proxy is not configured
     */
    getDeviceCode(scopes = []) {
        return this._authorization.getDeviceCode(scopes);
    }

    /**
     * See {@link Authorization#pollDeviceCode} for description.
     *
     * @returns {Promise<AccessToken>}
     * @throws {TypeError} if an OAuth proxy is not configured or `getDeviceCode()` was not previously called
     */
    pollDeviceCode() {
        return this._authorization.pollDeviceCode();
    }

    /* Listening */

    /**
     * See {@link Listening#getRecommendation} for description.
     *
     * @param {string} [uid='']           Optional; a UID for a specific recommendation to play. In 99% of use cases, this is not needed.
     * @param {string} [channel='npr']    Optional; a channel to pull the recommendation from; the main flow channel of `npr` is used as the default. In 99% of use cases, this does not need to be changed.
     * @returns {Promise<Recommendation>}
     */
    getRecommendation(uid = '', channel = 'npr') {
        return this._listening.getRecommendation(uid, channel);
    }

    /**
     * See {@link Listening#resumeFlowFromRecommendation} for description.
     *
     * @param {Object} json JSON object representation of a recommendation
     * @returns {Recommendation}
     */
    resumeFlowFromRecommendation(json) {
        return this._listening.resumeFlowFromRecommendation(json);
    }

    /**
     * See {@link Listening#getUpcomingFlowRecommendations} for description.
     *
     * @experimental
     * @param {string} [channel='npr']   A channel to pull the next recommendation from
     * @returns {Promise<Array<Recommendation>>}
     */
    getUpcomingFlowRecommendations(channel = 'npr') {
        return this._listening.getUpcomingFlowRecommendations(channel);
    }

    /**
     * See {@link Listening#getRecommendationsFromChannel} for description.
     *
     * @param {string} [channel='recommended']   A non-flow (i.e. non-`npr`) channel to retrieve a list of recommendations from
     * @returns {Promise<Array<Recommendation>>}
     */
    getRecommendationsFromChannel(channel = 'recommended') {
        return this._listening.getRecommendationsFromChannel(channel);
    }

    /**
     * See {@link Listening#queueRecommendationFromChannel} for description.
     *
     * @param {string} channel   The channel used in the original call to `getRecommendationsFromChannel()`
     * @param {string} uid       The unique ID of the item to queue up for the user
     * @returns {Recommendation}
     * @throws {TypeError} If no valid channel or UID is passed in
     * @throws {Error} If no recommendations for this channel were previously cached, or if the UID was not found in that cached list
     */
    queueRecommendationFromChannel(channel, uid) {
        return this._listening.queueRecommendationFromChannel(channel, uid);
    }

    /**
     * See {@link Listening#getHistory} for description.
     *
     * @returns {Promise<Array<Recommendation>>}
     */
    getHistory() {
        return this._listening.getHistory();
    }

    /**
     * See {@link Listening#resetFlow} for description.
     *
     * @returns {Promise}
     */
    resetFlow() {
        return this._listening.resetFlow();
    }

    /* Identity */

    /**
     * See {@link Identity#getUser} for description.
     *
     * @returns {Promise<User>}
     */
    getUser() {
        return this._identity.getUser();
    }

    /**
     * See {@link Identity#setUserStation} for description.
     *
     * @param {number|string} stationId   The station's ID, which is either an integer or a numeric string (e.g. `123` or `'123'`)
     * @returns {Promise<User>}
     */
    setUserStation(stationId) {
        return this._identity.setUserStation(stationId);
    }

    /**
     * See {@link Identity#followShow} for description.
     *
     * @param {number|string} aggregationId    The aggregation (show) ID, which is either an integer or a numeric string (e.g. `123` or `'123'`)
     * @returns {Promise<User>}
     * @throws {TypeError} if the passed-in aggregation (show) ID is not either a number or a numeric string
     */
    followShow(aggregationId) {
        return this._identity.followShow(aggregationId);
    }

    /**
     * See {@link Identity#unfollowShow} for description.
     *
     * @param {number|string} aggregationId    The aggregation (show) ID, which is either an integer or a numeric string (e.g. `123` or `'123'`)
     * @returns {Promise<User>}
     * @throws {TypeError} if the passed-in aggregation (show) ID is not either a number or a numeric string
     */
    unfollowShow(aggregationId) {
        return this._identity.unfollowShow(aggregationId);
    }

    /**
     * See {@link Identity#createTemporaryUser} for description.
     *
     * @returns {Promise<User>}
     * @throws {TypeError} if an OAuth proxy is not configured or no client ID is set
     */
    createTemporaryUser() {
        return this._identity.createTemporaryUser();
    }

    /* Station Finder */

    /**
     * See {@link StationFinder#searchStations} for description.
     *
     * @param {null|string} [query]   An optional query, which can be a station name, network name, or zip code
     * @returns {Promise<Array<Station>>}
     */
    searchStations(query = null) {
        return this._stationfinder.searchStations(query);
    }

    /**
     * See {@link StationFinder#searchStationsByLatLongCoordinates} for description.
     *
     * @param {number} lat    A float representing the latitude value of the geographic coordinates
     * @param {number} long   A float representing the longitude value of the geographic coordinates
     * @returns {Promise<Array<Station>>}
     */
    searchStationsByLatLongCoordinates(lat, long) {
        return this._stationfinder.searchStationsByLatLongCoordinates(lat, long);
    }

    /**
     * See {@link StationFinder#searchStationsByCityAndState} for description.
     *
     * @param {string} city     A full city name (e.g. "New York", "San Francisco", "Phoenix")
     * @param {string} state    A state name (e.g. "Maryland") or abbreviation (e.g. "MD")
     * @returns {Promise<Array<Station>>}
     */
    searchStationsByCityAndState(city, state) {
        return this._stationfinder.searchStationsByCityAndState(city, state);
    }

    /**
     * See {@link StationFinder#searchStationsByCity} for description.
     *
     * @param {string} city   A full city name (e.g. "New York", "San Francisco", "Phoenix")
     * @returns {Promise<Array<Station>>}
     */
    searchStationsByCity(city) {
        return this._stationfinder.searchStationsByCity(city);
    }

    /**
     * See {@link StationFinder#searchStationsByState} for description.
     *
     * @param {string} state    A state name (e.g. "Maryland") or abbreviation (e.g. "MD")
     * @returns {Promise<Array<Station>>}
     */
    searchStationsByState(state) {
        return this._stationfinder.searchStationsByState(state);
    }

    /**
     * See {@link StationFinder#getStationDetails} for description.
     *
     * @param {number|string} stationId   The station's ID, which is either an integer or a numeric string (e.g. `123` or `'123'`)
     * @returns {Promise<Station>}
     */
    getStationDetails(stationId) {
        return this._stationfinder.getStationDetails(stationId);
    }

    /**
     * Returns the foundational path for a given service
     *
     * @param {string} service
     * @returns {string}
     * @throws {TypeError} if the passed-in service name is missing or invalid
     */
    static getServiceUrl(service) {
        switch (service) {
            case 'authorization':
                return `https://${NprOneSDK.config.subdomain}authorization.api.npr.org/v2`;
            case 'identity':
                return `https://${NprOneSDK.config.subdomain}identity.api.npr.org/v2`;
            case 'listening':
                return `https://${NprOneSDK.config.subdomain}listening.api.npr.org/v2`;
            case 'stationfinder':
                return `https://${NprOneSDK.config.subdomain}station.api.npr.org/v3`;
            default:
                throw new TypeError('Must specify a valid service name');
        }
    }

    /**
     * Initializes the config using default settings.
     *
     * @private
     */
    static _initConfig() {
        if (!NprOneSDK._config) {
            NprOneSDK._config = {
                authProxyBaseUrl: '',
                newDeviceCodePath: '/device',
                pollDeviceCodePath: '/device/poll',
                refreshTokenPath: '/refresh',
                tempUserPath: '/temporary',
                logoutPath: '/logout',
                accessToken: '',
                clientId: '',
                advertisingId: '',
                advertisingTarget: '',
                subdomain: '',
            };
        }
    }
}
export default NprOneSDK;

/**
 * @external {Response} https://developer.mozilla.org/en-US/docs/Web/API/Response
 */
/**
 * @external {Headers} https://developer.mozilla.org/en-US/docs/Web/API/Headers
 */
/**
 * @external {JsLogger} https://github.com/jonnyreeves/js-logger
 */