-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
URO-206 getting started with orcidlink integration #206
Changes from all commits
0ed2b8d
73f086a
9d1eebb
c003f80
682f010
3935be2
7e4987a
bcbf09a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
import { baseApi } from '.'; | ||
import { jsonRpcService } from './utils/serviceHelpers'; | ||
|
||
// orcidlink system types | ||
|
||
export interface ORCIDAuthPublic { | ||
expires_in: number; | ||
name: string; | ||
orcid: string; | ||
scope: string; | ||
} | ||
|
||
export interface LinkRecordPublic { | ||
created_at: number; | ||
expires_at: number; | ||
retires_at: number; | ||
username: string; | ||
orcid_auth: ORCIDAuthPublic; | ||
} | ||
|
||
// Method types | ||
|
||
export interface StatusResult { | ||
status: string; | ||
current_time: number; | ||
start_time: number; | ||
} | ||
|
||
export interface InfoResult { | ||
'service-description': { | ||
name: string; | ||
title: string; | ||
version: string; | ||
}; | ||
} | ||
|
||
// is-linked | ||
|
||
export interface IsLinkedParams { | ||
username: string; | ||
} | ||
|
||
export type IsLinkedResult = boolean; | ||
|
||
// owner-link | ||
export interface OwnerLinkParams { | ||
username: string; | ||
} | ||
|
||
export type OwnerLinkResult = LinkRecordPublic; | ||
|
||
// It is mostly a JSONRPC 2.0 service, although the oauth flow is rest-ish. | ||
const orcidlinkService = jsonRpcService({ | ||
url: '/services/orcidlink/api/v1', | ||
version: '2.0', | ||
}); | ||
|
||
/** | ||
* orcidlink service api | ||
*/ | ||
export const orcidlinkAPI = baseApi | ||
.enhanceEndpoints({ addTagTypes: ['ORCIDLink'] }) | ||
.injectEndpoints({ | ||
endpoints: ({ query }) => ({ | ||
orcidlinkStatus: query<StatusResult, {}>({ | ||
query: () => { | ||
return orcidlinkService({ | ||
method: 'status', | ||
}); | ||
}, | ||
}), | ||
orcidlinkIsLinked: query<IsLinkedResult, IsLinkedParams>({ | ||
query: ({ username }) => { | ||
return orcidlinkService({ | ||
method: 'is-linked', | ||
params: { | ||
username, | ||
}, | ||
}); | ||
}, | ||
}), | ||
orcidlinkOwnerLink: query<OwnerLinkResult, OwnerLinkParams>({ | ||
query: ({ username }) => { | ||
return orcidlinkService({ | ||
method: 'owner-link', | ||
params: { | ||
username, | ||
}, | ||
}); | ||
}, | ||
}), | ||
}), | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,7 +7,7 @@ import { | |
} from '@reduxjs/toolkit/query/react'; | ||
import { RootState } from '../../../app/store'; | ||
import { serviceWizardApi } from '../serviceWizardApi'; | ||
import { KBaseBaseQueryError, isJsonRpcError } from './common'; | ||
import { isJsonRpcError, KBaseBaseQueryError } from './common'; | ||
|
||
export interface DynamicService { | ||
name: string; | ||
|
@@ -16,16 +16,33 @@ export interface DynamicService { | |
|
||
export interface StaticService { | ||
url: string; | ||
version?: '1.1' | '2.0'; | ||
} | ||
|
||
export interface JsonRpcQueryArgs { | ||
apiType: 'JsonRpc'; | ||
service: StaticService | DynamicService; | ||
method: string; | ||
params: unknown; | ||
params?: unknown; | ||
fetchArgs?: FetchArgs; | ||
} | ||
|
||
export interface JSONRPC11Body { | ||
version: string; | ||
id: string; | ||
method: string; | ||
params?: unknown; | ||
} | ||
|
||
export interface JSONRPC20Body { | ||
jsonrpc: string; | ||
id: string; | ||
method: string; | ||
params?: unknown; | ||
} | ||
|
||
export type JSONRPCBody = JSONRPC11Body | JSONRPC20Body; | ||
|
||
export interface HttpQueryArgs extends FetchArgs { | ||
apiType: 'Http'; | ||
service: StaticService; | ||
|
@@ -172,19 +189,35 @@ export const kbaseBaseQuery: ( | |
// Generate JsonRpc request id | ||
const reqId = Math.random().toString(); | ||
|
||
const body = ((): JSONRPCBody => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice! |
||
switch (kbQueryArgs.service.version || '1.1') { | ||
case '1.1': | ||
return { | ||
version: '1.1', | ||
id: reqId, | ||
method: kbQueryArgs.method, | ||
}; | ||
case '2.0': | ||
return { | ||
jsonrpc: '2.0', | ||
id: reqId, | ||
method: kbQueryArgs.method, | ||
}; | ||
} | ||
})(); | ||
|
||
if (kbQueryArgs.params) { | ||
body.params = kbQueryArgs.params; | ||
} | ||
|
||
// generate request body | ||
const fetchArgs = { | ||
url: new URL( | ||
kbQueryArgs.service.url, | ||
fetchBaseQueryArgs.baseUrl | ||
).toString(), | ||
method: 'POST', | ||
body: { | ||
version: '1.1', // TODO: conditionally implement JsonRpc 2.0 | ||
id: reqId, | ||
method: kbQueryArgs.method, | ||
params: kbQueryArgs.params, | ||
}, | ||
body, | ||
...kbQueryArgs.fetchArgs, // Allow overriding JsonRpc defaults | ||
}; | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import Alert from '@mui/material/Alert'; | ||
import { SerializedError } from '@reduxjs/toolkit'; | ||
import { KBaseBaseQueryError } from '../../common/api/utils/common'; | ||
import styles from './orcidlink.module.scss'; | ||
|
||
export interface ErrorMessageProps { | ||
error: KBaseBaseQueryError | SerializedError; | ||
} | ||
|
||
export default function ErrorMessage({ error }: ErrorMessageProps) { | ||
const message = (() => { | ||
if ('status' in error) { | ||
switch (error.status) { | ||
case 'JSONRPC_ERROR': | ||
return error.data.error.message; | ||
case 'FETCH_ERROR': | ||
return 'Fetch Error'; | ||
case 'CUSTOM_ERROR': | ||
return error.error; | ||
case 'PARSING_ERROR': | ||
return error.error; | ||
case 'TIMEOUT_ERROR': | ||
return error.error; | ||
} | ||
} else { | ||
return error.message || 'Unknown Error'; | ||
} | ||
Comment on lines
+11
to
+27
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Check out the parseError utility for this functionality. It may need to be updated to support exactly what you want, but will be reusable There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. src/common/api/utils/parseError.ts There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We tend to avoid switch (error.status) {
case 'JSONRPC_ERROR':
return error.data.error.message;
case 'FETCH_ERROR':
return 'Fetch Error';
case 'CUSTOM_ERROR':
case 'PARSING_ERROR':
case 'TIMEOUT_ERROR':
default:
return error.error;
} I think many other There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can abide by the codebase conventions. Yeah, first pass was not to use fallthrough because I hadn't explored those error conditions to see if there was more specialized information before; e.t. timeout in theory should have information about the timeout like the timeout limit and actual elapsed time. But, like I said, I didn't have time on this batch to create a test to exercise the conditions. |
||
})(); | ||
return ( | ||
<div className={styles['error-message']}> | ||
<Alert severity="error" title="Error"> | ||
{message} | ||
</Alert> | ||
</div> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
import { Alert, AlertTitle, CircularProgress } from '@mui/material'; | ||
import { SerializedError } from '@reduxjs/toolkit'; | ||
import { orcidlinkAPI } from '../../common/api/orcidlinkAPI'; | ||
import { KBaseBaseQueryError } from '../../common/api/utils/common'; | ||
import { useAppSelector } from '../../common/hooks'; | ||
import { authUsername } from '../auth/authSlice'; | ||
import { usePageTitle } from '../layout/layoutSlice'; | ||
import ErrorMessage from './ErrorMessage'; | ||
import Linked from './Linked'; | ||
import styles from './orcidlink.module.scss'; | ||
import Unlinked from './Unlinked'; | ||
|
||
export default function Home() { | ||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion | ||
const username = useAppSelector(authUsername)!; | ||
|
||
function renderLoading(title: string, description: string) { | ||
return ( | ||
<div className={styles.loading}> | ||
<Alert icon={<CircularProgress size="1rem" />}> | ||
<AlertTitle> | ||
<span className={styles['loading-title']}>{title}</span> | ||
</AlertTitle> | ||
<p>{description}</p> | ||
</Alert> | ||
</div> | ||
); | ||
} | ||
|
||
function renderError(error: KBaseBaseQueryError | SerializedError) { | ||
return <ErrorMessage error={error} />; | ||
} | ||
|
||
usePageTitle('KBase ORCID Link'); | ||
|
||
const { | ||
data: isLInked, | ||
error, | ||
isLoading, | ||
isError, | ||
isFetching, | ||
isSuccess, | ||
isUninitialized, | ||
} = orcidlinkAPI.useOrcidlinkIsLinkedQuery({ username }); | ||
|
||
if (isUninitialized) { | ||
return renderLoading('Uninitialized...', 'Loading the ORCID Link App...'); | ||
} else if (isLoading) { | ||
return renderLoading('Loading...', 'Loading the ORCID Link App...'); | ||
} else if (isFetching) { | ||
return renderLoading('Fetching...', 'Loading the ORCID Link App...'); | ||
} else if (isError) { | ||
return renderError(error); | ||
} else if (isSuccess) { | ||
if (isLInked) { | ||
return ( | ||
<div className={styles.box}> | ||
<Linked /> | ||
</div> | ||
); | ||
} | ||
return ( | ||
<div className={styles.box}> | ||
<Unlinked /> | ||
</div> | ||
); | ||
} else { | ||
return renderError({ | ||
status: 'CUSTOM_ERROR', | ||
error: 'Unknown State', | ||
}); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: You can hardcode the version string here, or use a template string type if we want to support minor versions
It will make discrimination (type guarding) between this type and the 2.0 type easier downstream
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In terms of versions, as far as I know there are only two relevant JSON-RPC versions for us:
1.1
and2.0
. Also,1.1
is not technically official (although it is used in many places in KBase). That being said, official documents describing their operation do exist, and I suggest that we hew as close to them as possible.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, agreed @dakotablair, although our JSON-RPC 1.1 is not the same as the working draft that inspired it. So our reference for that should be our implementation.
@dakotablair Yes, the version could be hardcoded for sure; I did have another set of changes that addressed this more fundamentally, but more loc.