Commit 46a31363 authored by Robert Knight's avatar Robert Knight

Convert `api` service to an ES class

Part of https://github.com/hypothesis/client/issues/3298
parent 4471182d
...@@ -13,7 +13,7 @@ import { withServices } from '../service-context'; ...@@ -13,7 +13,7 @@ import { withServices } from '../service-context';
* @prop {Annotation} annotation - * @prop {Annotation} annotation -
* The annotation object for this banner. This contains state about the flag count * The annotation object for this banner. This contains state about the flag count
* or its hidden value. * or its hidden value.
* @prop {Object} api - Injected service * @prop {import('../services/api').APIService} api
* @prop {import('../services/toast-messenger').ToastMessengerService} toastMessenger * @prop {import('../services/toast-messenger').ToastMessengerService} toastMessenger
*/ */
......
...@@ -9,7 +9,7 @@ import ThreadList from './ThreadList'; ...@@ -9,7 +9,7 @@ import ThreadList from './ThreadList';
/** /**
* @typedef StreamViewProps * @typedef StreamViewProps
* @prop {ReturnType<import('../services/api').default>} api * @prop {import('../services/api').APIService} api
* @prop {import('../services/toast-messenger').ToastMessengerService} toastMessenger * @prop {import('../services/toast-messenger').ToastMessengerService} toastMessenger
*/ */
......
...@@ -107,7 +107,7 @@ import { ServiceContext } from './service-context'; ...@@ -107,7 +107,7 @@ import { ServiceContext } from './service-context';
import bridgeService from '../shared/bridge'; import bridgeService from '../shared/bridge';
import { AnnotationsService } from './services/annotations'; import { AnnotationsService } from './services/annotations';
import apiService from './services/api'; import { APIService } from './services/api';
import { APIRoutesService } from './services/api-routes'; import { APIRoutesService } from './services/api-routes';
import { AuthService } from './services/auth'; import { AuthService } from './services/auth';
import { AutosaveService } from './services/autosave'; import { AutosaveService } from './services/autosave';
...@@ -144,7 +144,7 @@ function startApp(config, appEl) { ...@@ -144,7 +144,7 @@ function startApp(config, appEl) {
// Register services. // Register services.
container container
.register('annotationsService', AnnotationsService) .register('annotationsService', AnnotationsService)
.register('api', apiService) .register('api', APIService)
.register('apiRoutes', APIRoutesService) .register('apiRoutes', APIRoutesService)
.register('auth', AuthService) .register('auth', AuthService)
.register('autosaveService', AutosaveService) .register('autosaveService', AutosaveService)
......
...@@ -16,7 +16,7 @@ import { generateHexString } from '../util/random'; ...@@ -16,7 +16,7 @@ import { generateHexString } from '../util/random';
// @inject // @inject
export class AnnotationsService { export class AnnotationsService {
/** /**
* @param {ReturnType<import('./api').default>} api * @param {import('./api').APIService} api
* @param {import('../store').SidebarStore} store * @param {import('../store').SidebarStore} store
*/ */
constructor(api, store) { constructor(api, store) {
......
...@@ -14,7 +14,7 @@ import { replaceURLParams } from '../util/url'; ...@@ -14,7 +14,7 @@ import { replaceURLParams } from '../util/url';
*/ */
function translateResponseToError(response, data) { function translateResponseToError(response, data) {
let message = response.status + ' ' + response.statusText; let message = response.status + ' ' + response.statusText;
if (data && data.reason) { if (data?.reason) {
message = message + ': ' + data.reason; message = message + ': ' + data.reason;
} }
return new Error(message); return new Error(message);
...@@ -24,7 +24,7 @@ function translateResponseToError(response, data) { ...@@ -24,7 +24,7 @@ function translateResponseToError(response, data) {
* Return a shallow clone of `obj` with all client-only properties removed. * Return a shallow clone of `obj` with all client-only properties removed.
* Client-only properties are marked by a '$' prefix. * Client-only properties are marked by a '$' prefix.
* *
* @param {Object} obj * @param {object} obj
*/ */
function stripInternalProperties(obj) { function stripInternalProperties(obj) {
const result = {}; const result = {};
...@@ -61,7 +61,7 @@ function stripInternalProperties(obj) { ...@@ -61,7 +61,7 @@ function stripInternalProperties(obj) {
* *
* @callback APICallFunction * @callback APICallFunction
* @param {Record<string, any>} params - A map of URL and query string parameters to include with the request. * @param {Record<string, any>} params - A map of URL and query string parameters to include with the request.
* @param {Object} [data] - The body of the request. * @param {object} [data] - The body of the request.
* @param {APICallOptions} [options] * @param {APICallOptions} [options]
* @return {Promise<any|APIResponse>} * @return {Promise<any|APIResponse>}
*/ */
...@@ -69,7 +69,7 @@ function stripInternalProperties(obj) { ...@@ -69,7 +69,7 @@ function stripInternalProperties(obj) {
/** /**
* Callbacks invoked at various points during an API call to get an access token etc. * Callbacks invoked at various points during an API call to get an access token etc.
* *
* @typedef {Object} APIMethodCallbacks * @typedef APIMethodCallbacks
* @prop {() => Promise<string|null>} getAccessToken - * @prop {() => Promise<string|null>} getAccessToken -
* Function which acquires a valid access token for making an API request. * Function which acquires a valid access token for making an API request.
* @prop {() => string|null} getClientId - * @prop {() => string|null} getClientId -
...@@ -193,44 +193,41 @@ function createAPICall( ...@@ -193,44 +193,41 @@ function createAPICall(
* }); * });
* ``` * ```
* *
* This service handles authenticated calls to the API, using the `auth` service * This service makes authenticated calls to the API, using `AuthService`
* to get auth tokens. The URLs for API endpoints are fetched from the `/api` * to get auth tokens. The URLs for API endpoints are provided by the `APIRoutesService`
* endpoint, a responsibility delegated to the `apiRoutes` service which does * service.
* not use authentication.
*
* @param {import('./api-routes').APIRoutesService} apiRoutes
* @param {import('./auth').AuthService} auth
*/ */
// @inject // @inject
export default function api(apiRoutes, auth, store) { export class APIService {
const links = apiRoutes.routes(); /**
let clientId = null; * @param {import('./api-routes').APIRoutesService} apiRoutes
* @param {import('./auth').AuthService} auth
const getClientId = () => clientId; * @param {import('../store').SidebarStore} store
*/
constructor(apiRoutes, auth, store) {
const links = apiRoutes.routes();
function apiCall(route) {
return createAPICall(links, route, {
getAccessToken: auth.getAccessToken,
getClientId,
onRequestStarted: store.apiRequestStarted,
onRequestFinished: store.apiRequestFinished,
});
}
return {
/** /**
* Set the "client ID" sent with API requests. * Client session identifier included with requests. Used by the backend
* to associate API requests with WebSocket connections from the same client.
* *
* This is a per-session unique ID which the client sends with REST API * @type {string|null}
* requests and in the configuration for the real-time API to prevent the
* client from receiving real-time notifications about its own actions.
*/ */
setClientId(clientId_) { this._clientId = null;
clientId = clientId_;
}, const getClientId = () => this._clientId;
search: apiCall('search'), /** @param {string} route */
annotation: { const apiCall = route =>
createAPICall(links, route, {
getAccessToken: auth.getAccessToken,
getClientId,
onRequestStarted: store.apiRequestStarted,
onRequestFinished: store.apiRequestFinished,
});
this.search = apiCall('search');
this.annotation = {
create: apiCall('annotation.create'), create: apiCall('annotation.create'),
delete: apiCall('annotation.delete'), delete: apiCall('annotation.delete'),
get: apiCall('annotation.read'), get: apiCall('annotation.read'),
...@@ -238,25 +235,35 @@ export default function api(apiRoutes, auth, store) { ...@@ -238,25 +235,35 @@ export default function api(apiRoutes, auth, store) {
flag: apiCall('annotation.flag'), flag: apiCall('annotation.flag'),
hide: apiCall('annotation.hide'), hide: apiCall('annotation.hide'),
unhide: apiCall('annotation.unhide'), unhide: apiCall('annotation.unhide'),
}, };
group: { this.group = {
member: { member: {
delete: apiCall('group.member.delete'), delete: apiCall('group.member.delete'),
}, },
read: apiCall('group.read'), read: apiCall('group.read'),
}, };
groups: { this.groups = {
list: apiCall('groups.read'), list: apiCall('groups.read'),
}, };
profile: { this.profile = {
groups: { groups: {
read: apiCall('profile.groups.read'), read: apiCall('profile.groups.read'),
}, },
read: apiCall('profile.read'), read: apiCall('profile.read'),
update: apiCall('profile.update'), update: apiCall('profile.update'),
}, };
}
// The `links` endpoint is not included here. Clients should fetch these /**
// from the `apiRoutes` service. * Set the "client ID" sent with API requests.
}; *
* This is a per-session unique ID which the client sends with REST API
* requests and in the configuration for the real-time API to prevent the
* client from receiving real-time notifications about its own actions.
*
* @param {string} clientId
*/
setClientId(clientId) {
this._clientId = clientId;
}
} }
...@@ -22,6 +22,7 @@ const DEFAULT_ORGANIZATION = { ...@@ -22,6 +22,7 @@ const DEFAULT_ORGANIZATION = {
/** /**
* @param {import('../store').SidebarStore} store * @param {import('../store').SidebarStore} store
* @param {import('./api').APIService} api
* @param {import('./toast-messenger').ToastMessengerService} toastMessenger * @param {import('./toast-messenger').ToastMessengerService} toastMessenger
* @param {import('./auth').AuthService} auth * @param {import('./auth').AuthService} auth
*/ */
......
...@@ -30,7 +30,7 @@ import { SearchClient } from '../search-client'; ...@@ -30,7 +30,7 @@ import { SearchClient } from '../search-client';
import { isReply } from '../helpers/annotation-metadata'; import { isReply } from '../helpers/annotation-metadata';
/** /**
* @param {ReturnType<import('./api').default>} api * @param {import('./api').APIService} api
* @param {import('../store').SidebarStore} store * @param {import('../store').SidebarStore} store
* @param {import('./streamer').default} streamer * @param {import('./streamer').default} streamer
* @param {import('./stream-filter').StreamFilter} streamFilter * @param {import('./stream-filter').StreamFilter} streamFilter
......
...@@ -13,6 +13,7 @@ const CACHE_TTL = 5 * 60 * 1000; // 5 minutes ...@@ -13,6 +13,7 @@ const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
* Access to the current profile is exposed via the `state` property. * Access to the current profile is exposed via the `state` property.
* *
* @param {import('../store').SidebarStore} store * @param {import('../store').SidebarStore} store
* @param {import('./api').APIService} api
* @param {import('./auth').AuthService} auth * @param {import('./auth').AuthService} auth
* @param {import('./toast-messenger').ToastMessengerService} toastMessenger * @param {import('./toast-messenger').ToastMessengerService} toastMessenger
* @inject * @inject
......
import fetchMock from 'fetch-mock'; import fetchMock from 'fetch-mock';
import apiFactory from '../api'; import { APIService } from '../api';
// API route directory. // API route directory.
// //
...@@ -13,7 +13,7 @@ import apiFactory from '../api'; ...@@ -13,7 +13,7 @@ import apiFactory from '../api';
// //
const routes = require('./api-index.json').links; const routes = require('./api-index.json').links;
describe('sidebar.services.api', function () { describe('APIService', () => {
let fakeAuth; let fakeAuth;
let fakeStore; let fakeStore;
let api; let api;
...@@ -72,7 +72,7 @@ describe('sidebar.services.api', function () { ...@@ -72,7 +72,7 @@ describe('sidebar.services.api', function () {
apiRequestFinished: sinon.stub(), apiRequestFinished: sinon.stub(),
}; };
api = apiFactory(fakeApiRoutes, fakeAuth, fakeStore); api = new APIService(fakeApiRoutes, fakeAuth, fakeStore);
fetchMock.catch(() => { fetchMock.catch(() => {
throw new Error('Unexpected `fetch` call'); throw new Error('Unexpected `fetch` call');
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment