Commit 12042882 authored by Robert Knight's avatar Robert Knight

Add type information for API call methods.

Add `/** @type {APICall<...>} */` annotations to methods in `APIService`
to specify parameter, body and return types for methods that make API calls.
The existing `APICallFunction` type was renamed to `APICall` for
brevity.

This required adding a few additional casts in other modules to assert
that certain uses of possibly-undefined fields (eg. `annotation.id`) are
safe in a particular context.
parent 37a2a8bd
......@@ -56,7 +56,9 @@ function AnnotationView({ loadAnnotationsService, onLogin }) {
// Make the full thread of annotations visible. By default replies are
// not shown until the user expands the thread.
annots.forEach(annot => store.setExpanded(annot.id, true));
annots.forEach(annot =>
store.setExpanded(/** @type {string} */ (annot.id), true)
);
// FIXME - This should show a visual indication of which reply the
// annotation ID in the URL refers to. That isn't currently working.
......
......@@ -34,10 +34,11 @@ function ModerationBanner({ annotation, api, toastMessenger }) {
* Hide an annotation from non-moderator users.
*/
const hideAnnotation = () => {
const id = /** @type {string} */ (annotation.id);
api.annotation
.hide({ id: annotation.id })
.hide({ id })
.then(() => {
store.hideAnnotation(annotation.id);
store.hideAnnotation(id);
})
.catch(() => {
toastMessenger.error('Failed to hide annotation');
......@@ -48,10 +49,11 @@ function ModerationBanner({ annotation, api, toastMessenger }) {
* Un-hide an annotation from non-moderator users.
*/
const unhideAnnotation = () => {
const id = /** @type {string} */ (annotation.id);
api.annotation
.unhide({ id: annotation.id })
.unhide({ id })
.then(() => {
store.unhideAnnotation(annotation.id);
store.unhideAnnotation(id);
})
.catch(() => {
toastMessenger.error('Failed to unhide annotation');
......
......@@ -3,7 +3,10 @@ import * as queryString from 'query-string';
import { replaceURLParams } from '../util/url';
/**
* @typedef {import('../../types/api').Annotation} Annotation
* @typedef {import('../../types/api').Group} Group
* @typedef {import('../../types/api').RouteMap} RouteMap
* @typedef {import('../../types/api').Profile} Profile
*/
/**
......@@ -39,8 +42,9 @@ function stripInternalProperties(obj) {
}
/**
* @template {object} Body
* @typedef APIResponse
* @prop {any} data -
* @prop {Body} data -
* The JSON response from the API call, unless this call returned a
* "204 No Content" status.
* @prop {string|null} token - The access token that was used to make the call
......@@ -59,11 +63,14 @@ function stripInternalProperties(obj) {
/**
* Function which makes an API request.
*
* @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.
* @template {Record<string, any>} Params
* @template {object} Body
* @template Result
* @callback APICall
* @param {Params} params - A map of URL and query string parameters to include with the request.
* @param {Body} [data] - The body of the request.
* @param {APICallOptions} [options]
* @return {Promise<any|APIResponse>}
* @return {Promise<Result>}
*/
/**
......@@ -93,7 +100,7 @@ function get(object, path) {
* @param {Promise<RouteMap>} links - API route data from API index endpoint (`/api/`)
* @param {string} route - The dotted path of the named API route (eg. `annotation.create`)
* @param {APIMethodCallbacks} callbacks
* @return {APICallFunction}
* @return {APICall<Record<string, any>, object, object>}
*/
function createAPICall(
links,
......@@ -182,8 +189,8 @@ function createAPICall(
* API client for the Hypothesis REST API.
*
* Returns an object that with keys that match the routes in
* the Hypothesis API (see http://h.readthedocs.io/en/latest/api/). See
* `APICallFunction` for the syntax of API calls. For example:
* the Hypothesis API (see http://h.readthedocs.io/en/latest/api/).
* @see APICall for the syntax of API calls. For example:
*
* ```
* api.annotations.update({ id: '1234' }, annotation).then(ann => {
......@@ -226,30 +233,70 @@ export class APIService {
onRequestFinished: store.apiRequestFinished,
});
// Define available API calls.
//
// The type syntax is APICall<Parameters, Body, Result>, where `void` means
// no body / empty response.
/**
* @typedef AnnotationSearchResult
* @prop {Annotation[]} rows
* @prop {Annotation[]} replies
* @prop {number} total
*/
/** @type {APICall<object, void, AnnotationSearchResult>} */
this.search = apiCall('search');
this.annotation = {
/** @type {APICall<{}, Partial<Annotation>, Annotation>} */
create: apiCall('annotation.create'),
/** @type {APICall<{ id: string }, void, void>} */
delete: apiCall('annotation.delete'),
/** @type {APICall<{ id: string }, void, Annotation>} */
get: apiCall('annotation.read'),
/** @type {APICall<{ id: string }, Partial<Annotation>, Annotation>} */
update: apiCall('annotation.update'),
/** @type {APICall<{ id: string }, void, void>} */
flag: apiCall('annotation.flag'),
/** @type {APICall<{ id: string }, void, void>} */
hide: apiCall('annotation.hide'),
/** @type {APICall<{ id: string }, void, void>} */
unhide: apiCall('annotation.unhide'),
};
this.group = {
member: {
/** @type {APICall<{ pubid: string, userid: string }, void, void>} */
delete: apiCall('group.member.delete'),
},
/** @type {APICall<{ id: string, expand: string[] }, void, Group>} */
read: apiCall('group.read'),
};
/**
* @typedef ListGroupParams
* @prop {string} [authority]
* @prop {string} [document_uri]
* @prop {string[]} [expand]
*/
this.groups = {
/** @type {APICall<ListGroupParams, void, Group[]>} */
list: apiCall('groups.read'),
};
this.profile = {
groups: {
/** @type {APICall<{ expand: string[] }, void, Group[]>} */
read: apiCall('profile.groups.read'),
},
/** @type {APICall<{ authority?: string }, void, Profile>} */
read: apiCall('profile.read'),
/** @type {APICall<{}, Partial<Profile>, Profile>} */
update: apiCall('profile.update'),
};
}
......
......@@ -378,8 +378,9 @@ export class GroupsService {
userGroups.find(g => g.id === id || g.groupid === id) ||
tryFetchGroup(id);
const groups = (await Promise.all(groupIds.map(getGroup))).filter(
g => g !== null
const groupResults = await Promise.all(groupIds.map(getGroup));
const groups = /** @type {Group[]} */ (
groupResults.filter(g => g !== null)
);
// Optional direct linked group id. This is used in the Notebook context.
......
......@@ -180,7 +180,7 @@ export class LoadAnnotationsService {
// fetch the top-level annotation
if (isReply(annotation)) {
annotation = await this._api.annotation.get({
id: annotation.references[0],
id: /** @type {string[]} */ (annotation.references)[0],
});
}
......@@ -198,9 +198,10 @@ export class LoadAnnotationsService {
// configure the connection to the real-time update service to send us
// updates to any of the annotations in the thread.
if (!isReply(annotation)) {
const id = /** @type {string} */ (annotation.id);
this._streamFilter
.addClause('/references', 'one_of', annotation.id, true)
.addClause('/id', 'equals', annotation.id, true);
.addClause('/references', 'one_of', id, true)
.addClause('/id', 'equals', id, true);
this._streamer.setConfig('filter', {
filter: this._streamFilter.getFilter(),
});
......
......@@ -142,6 +142,7 @@
*
* @typedef Group
* @prop {string} id
* @prop {string} groupid
* @prop {'private'|'open'} type
* @prop {Organization} organization - nb. This field is nullable in the API, but
* we assign a default organization on the client.
......
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