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
{
fetchJSON
}
from
'../util/fetch'
;
import
{
replaceURLParams
}
from
'../util/url'
;
import
{
replaceURLParams
}
from
'../util/url'
;
import
type
{
APIRoutesService
}
from
'./api-routes'
;
/**
import
type
{
AuthService
}
from
'./auth'
;
* @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
*/
/**
/**
* 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 {Record<string, unknown>} obj
*/
*/
function
stripInternalProperties
(
obj
)
{
function
stripInternalProperties
(
/** @type {Record<string, unknown>} */
obj
:
Record
<
string
,
unknown
>
const
result
=
{};
):
Record
<
string
,
unknown
>
{
for
(
let
[
key
,
value
]
of
Object
.
entries
(
obj
))
{
const
result
:
Record
<
string
,
unknown
>
=
{};
for
(
const
[
key
,
value
]
of
Object
.
entries
(
obj
))
{
if
(
!
key
.
startsWith
(
'$'
))
{
if
(
!
key
.
startsWith
(
'$'
))
{
result
[
key
]
=
value
;
result
[
key
]
=
value
;
}
}
...
@@ -26,66 +27,59 @@ function stripInternalProperties(obj) {
...
@@ -26,66 +27,59 @@ function stripInternalProperties(obj) {
return
result
;
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.
* 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.
* Function which makes an API request.
*
* @param params - A map of URL and query string parameters to include with the request.
* @template {Record<string, Param|Param[]>} [Params={}]
* @param data - The body of the request.
* @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>}
*/
*/
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.
* Callbacks invoked at various points during an API call to get an access token etc.
*
*/
* @typedef APIMethodCallbacks
type
APIMethodCallbacks
=
{
* @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.
getAccessToken
:
()
=>
Promise
<
string
|
null
>
;
* @prop {() => string|null} getClientId -
/**
* Function that returns a per-session client ID to include with the request
* Function that returns a per-session client ID to include with the request
* or `null`.
* 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
;
/**
/** Callback invoked when the API request starts */
* @param {RouteMap|RouteMetadata} link
onRequestStarted
:
()
=>
void
;
* @return {link is RouteMetadata}
/** Callback invoked when the API request finishes */
*/
onRequestFinished
:
()
=>
void
;
function
isRouteMetadata
(
link
)
{
};
function
isRouteMetadata
(
link
:
RouteMap
|
RouteMetadata
):
link
is
RouteMetadata
{
return
'url'
in
link
;
return
'url'
in
link
;
}
}
/**
/**
* Lookup metadata for an API route in the result of an `/api/` response.
* Lookup metadata for an API route in the result of an `/api/` response.
*
*
* @param {RouteMap} routeMap
* @param route - Dot-separated path of route in `routeMap`
* @param {string} route - Dot-separated path of route in `routeMap`
*/
*/
function
findRouteMetadata
(
routeMap
,
route
)
{
function
findRouteMetadata
(
/** @type {RouteMap} */
routeMap
:
RouteMap
,
route
:
string
):
RouteMetadata
|
null
{
let
cursor
=
routeMap
;
let
cursor
=
routeMap
;
const
pathSegments
=
route
.
split
(
'.'
);
const
pathSegments
=
route
.
split
(
'.'
);
for
(
le
t
[
index
,
segment
]
of
pathSegments
.
entries
())
{
for
(
cons
t
[
index
,
segment
]
of
pathSegments
.
entries
())
{
const
nextCursor
=
cursor
[
segment
];
const
nextCursor
=
cursor
[
segment
];
if
(
!
nextCursor
||
isRouteMetadata
(
nextCursor
))
{
if
(
!
nextCursor
||
isRouteMetadata
(
nextCursor
))
{
if
(
nextCursor
&&
index
===
pathSegments
.
length
-
1
)
{
if
(
nextCursor
&&
index
===
pathSegments
.
length
-
1
)
{
...
@@ -104,18 +98,22 @@ function findRouteMetadata(routeMap, route) {
...
@@ -104,18 +98,22 @@ function findRouteMetadata(routeMap, route) {
/**
/**
* Creates a function that will make an API call to a named 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 links - API route data from API index endpoint (`/api/`)
* @param {string} route - The dotted path of the named API route (eg. `annotation.create`)
* @param route - The dotted path of the named API route (eg. `annotation.create`)
* @param {APIMethodCallbacks} callbacks
* @return Function that makes an API call. The returned `APICall` has generic
* @return {APICall<Record<string, any>, Record<string, any>|void, unknown>} - Function that makes
* parameter, body and return types.
* an API call. The returned `APICall` has generic parameter, body and return types.
* This can be cast to an `APICall` with more specific types.
* This can be cast to an `APICall` with more specific types.
*/
*/
function
createAPICall
(
function
createAPICall
(
links
,
links
:
Promise
<
RouteMap
>
,
route
,
route
:
string
,
{
getAccessToken
,
getClientId
,
onRequestStarted
,
onRequestFinished
}
{
)
{
getAccessToken
,
getClientId
,
onRequestStarted
,
onRequestFinished
,
}:
APIMethodCallbacks
):
APICall
<
Record
<
string
,
any
>
,
Record
<
string
,
any
>
|
void
,
unknown
>
{
return
async
(
params
,
data
)
=>
{
return
async
(
params
,
data
)
=>
{
onRequestStarted
();
onRequestStarted
();
try
{
try
{
...
@@ -125,8 +123,7 @@ function createAPICall(
...
@@ -125,8 +123,7 @@ function createAPICall(
throw
new
Error
(
`Missing API route:
${
route
}
`
);
throw
new
Error
(
`Missing API route:
${
route
}
`
);
}
}
/** @type {Record<string, string>} */
const
headers
:
Record
<
string
,
string
>
=
{
const
headers
=
{
'Content-Type'
:
'application/json'
,
'Content-Type'
:
'application/json'
,
'Hypothesis-Client-Version'
:
'__VERSION__'
,
'Hypothesis-Client-Version'
:
'__VERSION__'
,
};
};
...
@@ -146,11 +143,9 @@ function createAPICall(
...
@@ -146,11 +143,9 @@ function createAPICall(
);
);
const
apiURL
=
new
URL
(
url
);
const
apiURL
=
new
URL
(
url
);
for
(
let
[
key
,
value
]
of
Object
.
entries
(
queryParams
))
{
for
(
const
[
key
,
value
]
of
Object
.
entries
(
queryParams
))
{
if
(
!
Array
.
isArray
(
value
))
{
const
values
=
Array
.
isArray
(
value
)
?
value
:
[
value
];
value
=
[
value
];
for
(
const
item
of
values
)
{
}
for
(
let
item
of
value
)
{
// eslint-disable-next-line eqeqeq
// eslint-disable-next-line eqeqeq
if
(
item
==
null
)
{
if
(
item
==
null
)
{
// Skip all parameters with nullish values.
// Skip all parameters with nullish values.
...
@@ -175,6 +170,22 @@ function createAPICall(
...
@@ -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.
* API client for the Hypothesis REST API.
*
*
...
@@ -196,26 +207,49 @@ function createAPICall(
...
@@ -196,26 +207,49 @@ function createAPICall(
*/
*/
// @inject
// @inject
export
class
APIService
{
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
* Client session identifier included with requests. Used by the backend
* to associate API requests with WebSocket connections from the same client.
* 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
;
this
.
_clientId
=
null
;
const
links
=
apiRoutes
.
routes
();
const
getClientId
=
()
=>
this
.
_clientId
;
const
getClientId
=
()
=>
this
.
_clientId
;
const
apiCall
=
(
route
:
string
)
=>
/** @param {string} route */
const
apiCall
=
route
=>
createAPICall
(
links
,
route
,
{
createAPICall
(
links
,
route
,
{
getAccessToken
:
()
=>
auth
.
getAccessToken
(),
getAccessToken
:
()
=>
auth
.
getAccessToken
(),
getClientId
,
getClientId
,
...
@@ -228,68 +262,62 @@ export class APIService {
...
@@ -228,68 +262,62 @@ export class APIService {
// The type syntax is APICall<Parameters, Body, Result>, where `void` means
// The type syntax is APICall<Parameters, Body, Result>, where `void` means
// no body / empty response.
// no body / empty response.
/**
this
.
search
=
apiCall
(
'search'
)
as
APICall
<
* @typedef AnnotationSearchResult
Record
<
string
,
unknown
>
,
* @prop {Annotation[]} rows
void
,
* @prop {Annotation[]} replies
AnnotationSearchResult
* @prop {number} total
>
;
*/
/** @typedef {{ id: string }} IDParam */
this
.
search
=
/** @type {APICall<{}, void, AnnotationSearchResult>} */
(
apiCall
(
'search'
)
);
this
.
annotation
=
{
this
.
annotation
=
{
create
:
/** @type {APICall<{}, Partial<Annotation>, Annotation>} */
(
create
:
apiCall
(
'annotation.create'
)
as
APICall
<
apiCall
(
'annotation.create'
)
Record
<
string
,
unknown
>
,
),
Partial
<
Annotation
>
,
delete
:
/** @type {APICall<IDParam>} */
(
apiCall
(
'annotation.delete'
)),
Annotation
get
:
/** @type {APICall<IDParam, void, Annotation>} */
(
>
,
apiCall
(
'annotation.read'
)
delete
:
apiCall
(
'annotation.delete'
)
as
APICall
<
IDParam
>
,
),
get
:
apiCall
(
'annotation.read'
)
as
APICall
<
IDParam
,
void
,
Annotation
>
,
update
:
/** @type {APICall<IDParam, Partial<Annotation>, Annotation>} */
(
update
:
apiCall
(
'annotation.update'
)
as
APICall
<
apiCall
(
'annotation.update'
)
IDParam
,
),
Partial
<
Annotation
>
,
flag
:
/** @type {APICall<IDParam>} */
(
apiCall
(
'annotation.flag'
)),
Annotation
hide
:
/** @type {APICall<IDParam>} */
(
apiCall
(
'annotation.hide'
)),
>
,
unhide
:
/** @type {APICall<IDParam>} */
(
apiCall
(
'annotation.unhide'
)),
flag
:
apiCall
(
'annotation.flag'
)
as
APICall
<
IDParam
>
,
hide
:
apiCall
(
'annotation.hide'
)
as
APICall
<
IDParam
>
,
unhide
:
apiCall
(
'annotation.unhide'
)
as
APICall
<
IDParam
>
,
};
};
this
.
group
=
{
this
.
group
=
{
member
:
{
member
:
{
delete
:
/** @type {APICall<{ pubid: string, userid: string }>} */
(
delete
:
apiCall
(
'group.member.delete'
)
as
APICall
<
{
apiCall
(
'group.member.delete'
)
pubid
:
string
;
),
userid
:
string
;
}
>
,
},
},
read
:
/** @type {APICall<{ id: string, expand: string[] }, void, Group>} */
(
read
:
apiCall
(
'group.read'
)
as
APICall
<
apiCall
(
'group.read'
)
{
id
:
string
;
expand
:
string
[]
},
),
void
,
Group
>
,
};
};
/**
* @typedef ListGroupParams
* @prop {string} [authority]
* @prop {string} [document_uri]
* @prop {string[]} [expand]
*/
this
.
groups
=
{
this
.
groups
=
{
list
:
/** @type {APICall<ListGroupParams, void, Group[]>} */
(
list
:
apiCall
(
'groups.read'
)
as
APICall
<
ListGroupParams
,
void
,
Group
[]
>
,
apiCall
(
'groups.read'
)
),
};
};
this
.
profile
=
{
this
.
profile
=
{
groups
:
{
groups
:
{
read
:
/** @type {APICall<{ expand: string[] }, void, Group[]>} */
(
read
:
apiCall
(
'profile.groups.read'
)
as
APICall
<
apiCall
(
'profile.groups.read'
)
{
expand
:
string
[]
},
),
void
,
Group
[]
>
,
},
},
read
:
/** @type {APICall<{ authority?: string }, void, Profile>} */
(
read
:
apiCall
(
'profile.read'
)
as
APICall
<
apiCall
(
'profile.read'
)
{
authority
?:
string
},
),
void
,
update
:
/** @type {APICall<{}, Partial<Profile>, Profile>} */
(
Profile
apiCall
(
'profile.update'
)
>
,
),
update
:
apiCall
(
'profile.update'
)
as
APICall
<
Record
<
string
,
unknown
>
,
Partial
<
Profile
>
,
Profile
>
,
};
};
}
}
...
@@ -299,10 +327,8 @@ export class APIService {
...
@@ -299,10 +327,8 @@ export class APIService {
* This is a per-session unique ID which the client sends with REST API
* 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
* requests and in the configuration for the real-time API to prevent the
* client from receiving real-time notifications about its own actions.
* client from receiving real-time notifications about its own actions.
*
* @param {string} clientId
*/
*/
setClientId
(
clientId
)
{
setClientId
(
clientId
:
string
)
{
this
.
_clientId
=
clientId
;
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