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';
* @prop {Annotation} annotation -
* The annotation object for this banner. This contains state about the flag count
* or its hidden value.
* @prop {Object} api - Injected service
* @prop {import('../services/api').APIService} api
* @prop {import('../services/toast-messenger').ToastMessengerService} toastMessenger
*/
......
......@@ -9,7 +9,7 @@ import ThreadList from './ThreadList';
/**
* @typedef StreamViewProps
* @prop {ReturnType<import('../services/api').default>} api
* @prop {import('../services/api').APIService} api
* @prop {import('../services/toast-messenger').ToastMessengerService} toastMessenger
*/
......
......@@ -107,7 +107,7 @@ import { ServiceContext } from './service-context';
import bridgeService from '../shared/bridge';
import { AnnotationsService } from './services/annotations';
import apiService from './services/api';
import { APIService } from './services/api';
import { APIRoutesService } from './services/api-routes';
import { AuthService } from './services/auth';
import { AutosaveService } from './services/autosave';
......@@ -144,7 +144,7 @@ function startApp(config, appEl) {
// Register services.
container
.register('annotationsService', AnnotationsService)
.register('api', apiService)
.register('api', APIService)
.register('apiRoutes', APIRoutesService)
.register('auth', AuthService)
.register('autosaveService', AutosaveService)
......
......@@ -16,7 +16,7 @@ import { generateHexString } from '../util/random';
// @inject
export class AnnotationsService {
/**
* @param {ReturnType<import('./api').default>} api
* @param {import('./api').APIService} api
* @param {import('../store').SidebarStore} store
*/
constructor(api, store) {
......
......@@ -14,7 +14,7 @@ import { replaceURLParams } from '../util/url';
*/
function translateResponseToError(response, data) {
let message = response.status + ' ' + response.statusText;
if (data && data.reason) {
if (data?.reason) {
message = message + ': ' + data.reason;
}
return new Error(message);
......@@ -24,7 +24,7 @@ function translateResponseToError(response, data) {
* Return a shallow clone of `obj` with all client-only properties removed.
* Client-only properties are marked by a '$' prefix.
*
* @param {Object} obj
* @param {object} obj
*/
function stripInternalProperties(obj) {
const result = {};
......@@ -61,7 +61,7 @@ function stripInternalProperties(obj) {
*
* @callback APICallFunction
* @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]
* @return {Promise<any|APIResponse>}
*/
......@@ -69,7 +69,7 @@ function stripInternalProperties(obj) {
/**
* 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 -
* Function which acquires a valid access token for making an API request.
* @prop {() => string|null} getClientId -
......@@ -193,44 +193,41 @@ function createAPICall(
* });
* ```
*
* This service handles authenticated calls to the API, using the `auth` service
* to get auth tokens. The URLs for API endpoints are fetched from the `/api`
* endpoint, a responsibility delegated to the `apiRoutes` service which does
* not use authentication.
*
* This service makes authenticated calls to the API, using `AuthService`
* to get auth tokens. The URLs for API endpoints are provided by the `APIRoutesService`
* service.
*/
// @inject
export class APIService {
/**
* @param {import('./api-routes').APIRoutesService} apiRoutes
* @param {import('./auth').AuthService} auth
* @param {import('../store').SidebarStore} store
*/
// @inject
export default function api(apiRoutes, auth, store) {
constructor(apiRoutes, auth, store) {
const links = apiRoutes.routes();
let clientId = null;
const getClientId = () => clientId;
/**
* Client session identifier included with requests. Used by the backend
* to associate API requests with WebSocket connections from the same client.
*
* @type {string|null}
*/
this._clientId = null;
function apiCall(route) {
return createAPICall(links, route, {
const getClientId = () => this._clientId;
/** @param {string} route */
const apiCall = route =>
createAPICall(links, route, {
getAccessToken: auth.getAccessToken,
getClientId,
onRequestStarted: store.apiRequestStarted,
onRequestFinished: store.apiRequestFinished,
});
}
return {
/**
* 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.
*/
setClientId(clientId_) {
clientId = clientId_;
},
search: apiCall('search'),
annotation: {
this.search = apiCall('search');
this.annotation = {
create: apiCall('annotation.create'),
delete: apiCall('annotation.delete'),
get: apiCall('annotation.read'),
......@@ -238,25 +235,35 @@ export default function api(apiRoutes, auth, store) {
flag: apiCall('annotation.flag'),
hide: apiCall('annotation.hide'),
unhide: apiCall('annotation.unhide'),
},
group: {
};
this.group = {
member: {
delete: apiCall('group.member.delete'),
},
read: apiCall('group.read'),
},
groups: {
};
this.groups = {
list: apiCall('groups.read'),
},
profile: {
};
this.profile = {
groups: {
read: apiCall('profile.groups.read'),
},
read: apiCall('profile.read'),
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 = {
/**
* @param {import('../store').SidebarStore} store
* @param {import('./api').APIService} api
* @param {import('./toast-messenger').ToastMessengerService} toastMessenger
* @param {import('./auth').AuthService} auth
*/
......
......@@ -30,7 +30,7 @@ import { SearchClient } from '../search-client';
import { isReply } from '../helpers/annotation-metadata';
/**
* @param {ReturnType<import('./api').default>} api
* @param {import('./api').APIService} api
* @param {import('../store').SidebarStore} store
* @param {import('./streamer').default} streamer
* @param {import('./stream-filter').StreamFilter} streamFilter
......
......@@ -13,6 +13,7 @@ const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
* Access to the current profile is exposed via the `state` property.
*
* @param {import('../store').SidebarStore} store
* @param {import('./api').APIService} api
* @param {import('./auth').AuthService} auth
* @param {import('./toast-messenger').ToastMessengerService} toastMessenger
* @inject
......
import fetchMock from 'fetch-mock';
import apiFactory from '../api';
import { APIService } from '../api';
// API route directory.
//
......@@ -13,7 +13,7 @@ import apiFactory from '../api';
//
const routes = require('./api-index.json').links;
describe('sidebar.services.api', function () {
describe('APIService', () => {
let fakeAuth;
let fakeStore;
let api;
......@@ -72,7 +72,7 @@ describe('sidebar.services.api', function () {
apiRequestFinished: sinon.stub(),
};
api = apiFactory(fakeApiRoutes, fakeAuth, fakeStore);
api = new APIService(fakeApiRoutes, fakeAuth, fakeStore);
fetchMock.catch(() => {
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