Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
C
coopwire-hypothesis
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
孙灵跃 Leon Sun
coopwire-hypothesis
Commits
15d64695
Commit
15d64695
authored
May 16, 2023
by
Alejandro Celaya
Committed by
Alejandro Celaya
May 16, 2023
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Migrate APIService to TS
parent
27ce4cd3
Changes
1
Show whitespace changes
Inline
Side-by-side
Showing
1 changed file
with
334 additions
and
0 deletions
+334
-0
api.ts
src/sidebar/services/api.ts
+334
-0
No files found.
src/sidebar/services/api.
j
s
→
src/sidebar/services/api.
t
s
View file @
15d64695
import
type
{
Annotation
,
Group
,
RouteMap
,
RouteMetadata
,
Profile
,
}
from
'../../types/api'
;
import
type
{
SidebarStore
}
from
'../store'
;
import
{
fetchJSON
}
from
'../util/fetch'
;
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').RouteMetadata} RouteMetadata
* @typedef {import('../../types/api').Profile} Profile
*/
import
type
{
APIRoutesService
}
from
'./api-routes'
;
import
type
{
AuthService
}
from
'./auth'
;
/**
* Return a shallow clone of `obj` with all client-only properties removed.
* Client-only properties are marked by a '$' prefix.
*
* @param {Record<string, unknown>} obj
*/
function
stripInternalProperties
(
obj
)
{
/** @type {Record<string, unknown>} */
const
result
=
{};
for
(
let
[
key
,
value
]
of
Object
.
entries
(
obj
))
{
function
stripInternalProperties
(
obj
:
Record
<
string
,
unknown
>
):
Record
<
string
,
unknown
>
{
const
result
:
Record
<
string
,
unknown
>
=
{};
for
(
const
[
key
,
value
]
of
Object
.
entries
(
obj
))
{
if
(
!
key
.
startsWith
(
'$'
))
{
result
[
key
]
=
value
;
}
...
...
@@ -26,66 +27,59 @@ function stripInternalProperties(obj) {
return
result
;
}
/**
* @template {object} Body
* @typedef APIResponse
* @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
* or `null` if unauthenticated.
*/
/**
* Types of value that can be passed as a parameter to API calls.
*
* @typedef {string|number|boolean} Param
*/
type
Param
=
string
|
number
|
boolean
;
/**
* Function which makes an API request.
*
* @template {Record<string, Param|Param[]>} [Params={}]
* @template [Body=void]
* @template [Result=void]
* @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.
* @return {Promise<Result>}
* @param params - A map of URL and query string parameters to include with the request.
* @param data - The body of the request.
*/
type
APICall
<
Params
=
Record
<
string
,
Param
|
Param
[]
>
,
Body
=
void
,
Result
=
void
>
=
(
params
:
Params
,
data
?:
Body
)
=>
Promise
<
Result
>
;
/**
* Callbacks invoked at various points during an API call to get an access token etc.
*
* @typedef APIMethodCallbacks
* @prop {() => Promise<string|null>} getAccessToken -
* Function which acquires a valid access token for making an API request.
* @prop {() => string|null} getClientId -
*/
type
APIMethodCallbacks
=
{
/** Function which acquires a valid access token for making an API request */
getAccessToken
:
()
=>
Promise
<
string
|
null
>
;
/**
* Function that returns a per-session client ID to include with the request
* or `null`.
* @prop {() => void} onRequestStarted - Callback invoked when the API request starts.
* @prop {() => void} onRequestFinished - Callback invoked when the API request finishes.
*/
getClientId
:
()
=>
string
|
null
;
/**
* @param {RouteMap|RouteMetadata} link
* @return {link is RouteMetadata}
*/
function
isRouteMetadata
(
link
)
{
/** Callback invoked when the API request starts */
onRequestStarted
:
()
=>
void
;
/** Callback invoked when the API request finishes */
onRequestFinished
:
()
=>
void
;
};
function
isRouteMetadata
(
link
:
RouteMap
|
RouteMetadata
):
link
is
RouteMetadata
{
return
'url'
in
link
;
}
/**
* Lookup metadata for an API route in the result of an `/api/` response.
*
* @param {RouteMap} routeMap
* @param {string} route - Dot-separated path of route in `routeMap`
* @param route - Dot-separated path of route in `routeMap`
*/
function
findRouteMetadata
(
routeMap
,
route
)
{
/** @type {RouteMap} */
function
findRouteMetadata
(
routeMap
:
RouteMap
,
route
:
string
):
RouteMetadata
|
null
{
let
cursor
=
routeMap
;
const
pathSegments
=
route
.
split
(
'.'
);
for
(
le
t
[
index
,
segment
]
of
pathSegments
.
entries
())
{
for
(
cons
t
[
index
,
segment
]
of
pathSegments
.
entries
())
{
const
nextCursor
=
cursor
[
segment
];
if
(
!
nextCursor
||
isRouteMetadata
(
nextCursor
))
{
if
(
nextCursor
&&
index
===
pathSegments
.
length
-
1
)
{
...
...
@@ -104,18 +98,22 @@ function findRouteMetadata(routeMap, route) {
/**
* Creates a function that will make an API call to a named route.
*
* @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 {APICall<Record<string, any>, Record<string, any>|void, unknown>} - Function that makes
* an API call. The returned `APICall` has generic parameter, body and return types.
* @param links - API route data from API index endpoint (`/api/`)
* @param route - The dotted path of the named API route (eg. `annotation.create`)
* @return Function that makes an API call. The returned `APICall` has generic
* parameter, body and return types.
* This can be cast to an `APICall` with more specific types.
*/
function
createAPICall
(
links
,
route
,
{
getAccessToken
,
getClientId
,
onRequestStarted
,
onRequestFinished
}
)
{
links
:
Promise
<
RouteMap
>
,
route
:
string
,
{
getAccessToken
,
getClientId
,
onRequestStarted
,
onRequestFinished
,
}:
APIMethodCallbacks
):
APICall
<
Record
<
string
,
any
>
,
Record
<
string
,
any
>
|
void
,
unknown
>
{
return
async
(
params
,
data
)
=>
{
onRequestStarted
();
try
{
...
...
@@ -125,8 +123,7 @@ function createAPICall(
throw
new
Error
(
`Missing API route:
${
route
}
`
);
}
/** @type {Record<string, string>} */
const
headers
=
{
const
headers
:
Record
<
string
,
string
>
=
{
'Content-Type'
:
'application/json'
,
'Hypothesis-Client-Version'
:
'__VERSION__'
,
};
...
...
@@ -146,11 +143,9 @@ function createAPICall(
);
const
apiURL
=
new
URL
(
url
);
for
(
let
[
key
,
value
]
of
Object
.
entries
(
queryParams
))
{
if
(
!
Array
.
isArray
(
value
))
{
value
=
[
value
];
}
for
(
let
item
of
value
)
{
for
(
const
[
key
,
value
]
of
Object
.
entries
(
queryParams
))
{
const
values
=
Array
.
isArray
(
value
)
?
value
:
[
value
];
for
(
const
item
of
values
)
{
// eslint-disable-next-line eqeqeq
if
(
item
==
null
)
{
// Skip all parameters with nullish values.
...
...
@@ -175,6 +170,22 @@ function createAPICall(
};
}
type
AnnotationSearchResult
=
{
rows
:
Annotation
[];
replies
:
Annotation
[];
total
:
number
;
};
type
IDParam
=
{
id
:
string
;
};
type
ListGroupParams
=
{
authority
?:
string
;
document_uri
?:
string
;
expand
?:
string
[];
};
/**
* API client for the Hypothesis REST API.
*
...
...
@@ -196,26 +207,49 @@ function createAPICall(
*/
// @inject
export
class
APIService
{
/**
* @param {import('./api-routes').APIRoutesService} apiRoutes
* @param {import('./auth').AuthService} auth
* @param {import('../store').SidebarStore} store
*/
constructor
(
apiRoutes
,
auth
,
store
)
{
const
links
=
apiRoutes
.
routes
();
/**
* Client session identifier included with requests. Used by the backend
* to associate API requests with WebSocket connections from the same client.
*
* @type {string|null}
*/
private
_clientId
:
string
|
null
;
search
:
APICall
<
Record
<
string
,
unknown
>
,
void
,
AnnotationSearchResult
>
;
annotation
:
{
create
:
APICall
<
Record
<
string
,
unknown
>
,
Partial
<
Annotation
>
,
Annotation
>
;
delete
:
APICall
<
IDParam
>
;
get
:
APICall
<
IDParam
,
void
,
Annotation
>
;
update
:
APICall
<
IDParam
,
Partial
<
Annotation
>
,
Annotation
>
;
flag
:
APICall
<
IDParam
>
;
hide
:
APICall
<
IDParam
>
;
unhide
:
APICall
<
IDParam
>
;
};
group
:
{
member
:
{
delete
:
APICall
<
{
pubid
:
string
;
userid
:
string
}
>
;
};
read
:
APICall
<
{
id
:
string
;
expand
:
string
[]
},
void
,
Group
>
;
};
groups
:
{
list
:
APICall
<
ListGroupParams
,
void
,
Group
[]
>
;
};
profile
:
{
groups
:
{
read
:
APICall
<
{
expand
:
string
[]
},
void
,
Group
[]
>
;
};
read
:
APICall
<
{
authority
?:
string
},
void
,
Profile
>
;
update
:
APICall
<
Record
<
string
,
unknown
>
,
Partial
<
Profile
>
,
Profile
>
;
};
constructor
(
apiRoutes
:
APIRoutesService
,
auth
:
AuthService
,
store
:
SidebarStore
)
{
this
.
_clientId
=
null
;
const
links
=
apiRoutes
.
routes
();
const
getClientId
=
()
=>
this
.
_clientId
;
/** @param {string} route */
const
apiCall
=
route
=>
const
apiCall
=
(
route
:
string
)
=>
createAPICall
(
links
,
route
,
{
getAccessToken
:
()
=>
auth
.
getAccessToken
(),
getClientId
,
...
...
@@ -228,68 +262,62 @@ export class APIService {
// 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
*/
/** @typedef {{ id: string }} IDParam */
this
.
search
=
/** @type {APICall<{}, void, AnnotationSearchResult>} */
(
apiCall
(
'search'
)
);
this
.
search
=
apiCall
(
'search'
)
as
APICall
<
Record
<
string
,
unknown
>
,
void
,
AnnotationSearchResult
>
;
this
.
annotation
=
{
create
:
/** @type {APICall<{}, Partial<Annotation>, Annotation>} */
(
apiCall
(
'annotation.create'
)
),
delete
:
/** @type {APICall<IDParam>} */
(
apiCall
(
'annotation.delete'
)),
get
:
/** @type {APICall<IDParam, void, Annotation>} */
(
apiCall
(
'annotation.read'
)
),
update
:
/** @type {APICall<IDParam, Partial<Annotation>, Annotation>} */
(
apiCall
(
'annotation.update'
)
),
flag
:
/** @type {APICall<IDParam>} */
(
apiCall
(
'annotation.flag'
)),
hide
:
/** @type {APICall<IDParam>} */
(
apiCall
(
'annotation.hide'
)),
unhide
:
/** @type {APICall<IDParam>} */
(
apiCall
(
'annotation.unhide'
)),
create
:
apiCall
(
'annotation.create'
)
as
APICall
<
Record
<
string
,
unknown
>
,
Partial
<
Annotation
>
,
Annotation
>
,
delete
:
apiCall
(
'annotation.delete'
)
as
APICall
<
IDParam
>
,
get
:
apiCall
(
'annotation.read'
)
as
APICall
<
IDParam
,
void
,
Annotation
>
,
update
:
apiCall
(
'annotation.update'
)
as
APICall
<
IDParam
,
Partial
<
Annotation
>
,
Annotation
>
,
flag
:
apiCall
(
'annotation.flag'
)
as
APICall
<
IDParam
>
,
hide
:
apiCall
(
'annotation.hide'
)
as
APICall
<
IDParam
>
,
unhide
:
apiCall
(
'annotation.unhide'
)
as
APICall
<
IDParam
>
,
};
this
.
group
=
{
member
:
{
delete
:
/** @type {APICall<{ pubid: string, userid: string }>} */
(
apiCall
(
'group.member.delete'
)
),
delete
:
apiCall
(
'group.member.delete'
)
as
APICall
<
{
pubid
:
string
;
userid
:
string
;
}
>
,
},
read
:
/** @type {APICall<{ id: string, expand: string[] }, void, Group>} */
(
apiCall
(
'group.read'
)
),
read
:
apiCall
(
'group.read'
)
as
APICall
<
{
id
:
string
;
expand
:
string
[]
},
void
,
Group
>
,
};
/**
* @typedef ListGroupParams
* @prop {string} [authority]
* @prop {string} [document_uri]
* @prop {string[]} [expand]
*/
this
.
groups
=
{
list
:
/** @type {APICall<ListGroupParams, void, Group[]>} */
(
apiCall
(
'groups.read'
)
),
list
:
apiCall
(
'groups.read'
)
as
APICall
<
ListGroupParams
,
void
,
Group
[]
>
,
};
this
.
profile
=
{
groups
:
{
read
:
/** @type {APICall<{ expand: string[] }, void, Group[]>} */
(
apiCall
(
'profile.groups.read'
)
),
read
:
apiCall
(
'profile.groups.read'
)
as
APICall
<
{
expand
:
string
[]
},
void
,
Group
[]
>
,
},
read
:
/** @type {APICall<{ authority?: string }, void, Profile>} */
(
apiCall
(
'profile.read'
)
),
update
:
/** @type {APICall<{}, Partial<Profile>, Profile>} */
(
apiCall
(
'profile.update'
)
),
read
:
apiCall
(
'profile.read'
)
as
APICall
<
{
authority
?:
string
},
void
,
Profile
>
,
update
:
apiCall
(
'profile.update'
)
as
APICall
<
Record
<
string
,
unknown
>
,
Partial
<
Profile
>
,
Profile
>
,
};
}
...
...
@@ -299,10 +327,8 @@ export class APIService {
* 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
)
{
setClientId
(
clientId
:
string
)
{
this
.
_clientId
=
clientId
;
}
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment