Skip to content

Commit

Permalink
Merge pull request #210 from kbase/URO-208
Browse files Browse the repository at this point in the history
URO-208: orcidlink main view
  • Loading branch information
eapearson authored May 20, 2024
2 parents dd08bd2 + 04defd8 commit 8d8864b
Show file tree
Hide file tree
Showing 50 changed files with 1,978 additions and 236 deletions.
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions public/assets/images/ORCID-iD_icon-vector.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
73 changes: 73 additions & 0 deletions src/common/api/orcidLinkCommon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
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;
}

export interface ORCIDAuthPublicNonOwner {
orcid: string;
name: string;
}

export interface LinkRecordPublicNonOwner {
username: string;
orcid_auth: ORCIDAuthPublicNonOwner;
}

// ORCID User Profile (our version)

export interface Affiliation {
name: string;
role: string;
startYear: string;
endYear: string | null;
}

export interface ORCIDFieldGroupBase {
private: boolean;
}

export interface ORCIDFieldGroupPrivate extends ORCIDFieldGroupBase {
private: true;
fields: null;
}

export interface ORCIDFieldGroupAccessible<T> extends ORCIDFieldGroupBase {
private: false;
fields: T;
}

export type ORCIDFieldGroup<T> =
| ORCIDFieldGroupPrivate
| ORCIDFieldGroupAccessible<T>;

export interface ORCIDNameFieldGroup {
firstName: string;
lastName: string | null;
creditName: string | null;
}

export interface ORCIDBiographyFieldGroup {
bio: string;
}

export interface ORCIDEmailFieldGroup {
emailAddresses: Array<string>;
}

export interface ORCIDProfile {
orcidId: string;
nameGroup: ORCIDFieldGroup<ORCIDNameFieldGroup>;
biographyGroup: ORCIDFieldGroup<ORCIDBiographyFieldGroup>;
emailGroup: ORCIDFieldGroup<ORCIDEmailFieldGroup>;
employment: Array<Affiliation>;
}
148 changes: 102 additions & 46 deletions src/common/api/orcidlinkAPI.ts
Original file line number Diff line number Diff line change
@@ -1,53 +1,64 @@
import { baseApi } from '.';
import { LinkRecordPublic, ORCIDProfile } from './orcidLinkCommon';
import { jsonRpcService } from './utils/serviceHelpers';

// orcidlink system types
// system info

export interface ORCIDAuthPublic {
expires_in: number;
export interface ServiceDescription {
name: string;
orcid: string;
scope: string;
title: string;
version: string;
language: string;
description: string;
repoURL: string;
}

export interface LinkRecordPublic {
created_at: number;
expires_at: number;
retires_at: number;
username: string;
orcid_auth: ORCIDAuthPublic;
export interface GitInfo {
commit_hash: string;
commit_hash_abbreviated: string;
author_name: string;
committer_name: string;
committer_date: number;
url: string;
branch: string;
tag: string | null;
}

// Method types

export interface StatusResult {
status: string;
export interface RuntimeInfo {
current_time: number;
start_time: number;
orcid_api_url: string;
orcid_oauth_url: string;
orcid_site_url: string;
}

// TODO: normalize to either kebab or underscore. Pref underscore.
export interface InfoResult {
'service-description': {
name: string;
title: string;
version: string;
};
'service-description': ServiceDescription;
'git-info': GitInfo;
runtime_info: RuntimeInfo;
}

// is-linked
// combined api calls for initial view

export interface ORCIDLinkInitialStateResult {
isLinked: boolean;
info: InfoResult;
}

export interface IsLinkedParams {
export interface ORCIDLinkInitialStateParams {
username: string;
}

export type IsLinkedResult = boolean;
// combined api call for linked user info

// owner-link
export interface OwnerLinkParams {
username: string;
export interface ORCIDLinkLinkedUserInfoResult {
linkRecord: LinkRecordPublic;
profile: ORCIDProfile;
}

export type OwnerLinkResult = LinkRecordPublic;
export interface ORCIDLinkLinkedUserInfoParams {
username: string;
}

// It is mostly a JSONRPC 2.0 service, although the oauth flow is rest-ish.
const orcidlinkService = jsonRpcService({
Expand All @@ -62,31 +73,76 @@ 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,
orcidlinkInitialState: query<
ORCIDLinkInitialStateResult,
ORCIDLinkInitialStateParams
>({
async queryFn({ username }, _queryApi, _extraOptions, fetchWithBQ) {
const [isLinked, info] = await Promise.all([
fetchWithBQ(
orcidlinkService({
method: 'is-linked',
params: {
username,
},
})
),
fetchWithBQ(
orcidlinkService({
method: 'info',
})
),
]);
if (isLinked.error) {
return { error: isLinked.error };
}
if (info.error) {
return { error: info.error };
}
return {
data: {
isLinked: isLinked.data as boolean,
info: info.data as InfoResult,
},
});
};
},
}),
orcidlinkOwnerLink: query<OwnerLinkResult, OwnerLinkParams>({
query: ({ username }) => {
return orcidlinkService({
method: 'owner-link',
orcidlinkLinkedUserInfo: query<
ORCIDLinkLinkedUserInfoResult,
ORCIDLinkLinkedUserInfoParams
>({
async queryFn({ username }, _queryApi, _extraOptions, fetchWithBQ) {
const profileQuery = orcidlinkService({
method: 'get-orcid-profile',
params: {
username,
},
});

const [linkRecord, profile] = await Promise.all([
fetchWithBQ(
orcidlinkService({
method: 'owner-link',
params: {
username,
},
})
),
fetchWithBQ(profileQuery),
]);
if (linkRecord.error) {
return { error: linkRecord.error };
}

if (profile.error) {
return { error: profile.error };
}
return {
data: {
linkRecord: linkRecord.data as LinkRecordPublic,
profile: profile.data as ORCIDProfile,
},
};
},
}),
}),
Expand Down
21 changes: 21 additions & 0 deletions src/common/api/utils/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,27 @@ export const isJsonRpcError = (obj: unknown): obj is JsonRpcError => {
return false;
};

export const isJsonRpc20Error = (obj: unknown): obj is JsonRpcError => {
if (
typeof obj === 'object' &&
obj !== null &&
['jsonrpc', 'error', 'id'].every((k) => k in obj)
) {
const { jsonrpc, error } = obj as { jsonrpc: string; error: unknown };
if (jsonrpc !== '2.0') {
return false;
}
if (
typeof error === 'object' &&
error !== null &&
['code', 'message'].every((k) => k in error)
) {
return true;
}
}
return false;
};

/**
* Type predicate to narrow an unknown error to `FetchBaseQueryError`
*/
Expand Down
30 changes: 29 additions & 1 deletion src/common/api/utils/kbaseBaseQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ import {
} from '@reduxjs/toolkit/query/react';
import { RootState } from '../../../app/store';
import { serviceWizardApi } from '../serviceWizardApi';
import { isJsonRpcError, KBaseBaseQueryError } from './common';
import {
isJsonRpc20Error,
isJsonRpcError,
KBaseBaseQueryError,
} from './common';

export interface DynamicService {
name: string;
Expand Down Expand Up @@ -41,6 +45,12 @@ export interface JSONRPC20Body {
params?: unknown;
}

export interface JSONRPC20Error {
code: number;
message: string;
data: unknown;
}

export type JSONRPCBody = JSONRPC11Body | JSONRPC20Body;

export interface HttpQueryArgs extends FetchArgs {
Expand Down Expand Up @@ -226,6 +236,7 @@ export const kbaseBaseQuery: (
const response = await request;

// identify and better differentiate jsonRpc errors
// This is for KBase's version of JSON-RPC 1.1
if (response.error && response.error.status === 500) {
if (isJsonRpcError(response.error.data)) {
if (response.error.data.id && response.error.data.id !== reqId) {
Expand All @@ -245,6 +256,23 @@ export const kbaseBaseQuery: (
};
}
}
if (isJsonRpc20Error(response.data)) {
if (response.data.id && response.data.id !== reqId) {
return {
error: {
status: 'CUSTOM_ERROR',
error: 'JsonRpcProtocolError',
data: `Response ID "${response.data.id}" !== Request ID "${reqId}"`,
},
};
}
return {
error: {
status: 'JSONRPC_ERROR',
data: response.data,
},
};
}

// If another error has occurred preventing a response, return default rtk-query response.
// This appropriately handles rtk-query internal errors
Expand Down
2 changes: 1 addition & 1 deletion src/common/api/utils/serviceHelpers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { JsonRpcQueryArgs, HttpQueryArgs } from './kbaseBaseQuery';
import { HttpQueryArgs, JsonRpcQueryArgs } from './kbaseBaseQuery';

// Helpers for adding service info to each query,
export const jsonRpcService = (
Expand Down
Loading

0 comments on commit 8d8864b

Please sign in to comment.