From 22658a546018160bc2f108221bf1f93d066558b6 Mon Sep 17 00:00:00 2001 From: Lennard Golsch Date: Thu, 12 Dec 2024 15:40:35 +0100 Subject: [PATCH] fixes --- src/auth/fetch-jwt.ts | 36 ++++++--- src/common/cache/cache.ts | 43 +++++++++++ src/common/cache/memory-cache.ts | 23 ++++++ src/common/error.ts | 11 ++- src/common/url.ts | 35 ++++++--- src/i18n/lang-service-impl.ts | 63 ++++++++++++---- src/i18n/lang-service.ts | 21 ++++++ src/orcid/orcid-oauth.ts | 43 +++++++---- src/orcid/orcid-user.ts | 125 +++++++++++++++++++++++++------ src/orcid/orcid-work.ts | 98 ++++++++++++++++++++---- 10 files changed, 403 insertions(+), 95 deletions(-) diff --git a/src/auth/fetch-jwt.ts b/src/auth/fetch-jwt.ts index 4a9ddaa..02c7a50 100644 --- a/src/auth/fetch-jwt.ts +++ b/src/auth/fetch-jwt.ts @@ -17,28 +17,40 @@ */ import { createUrl } from '../common/url'; +import { handleError } from '../common/error'; interface MCRJWTResponse { login_success: boolean; access_token: string; } -const fetchJWT = async(baseUrl: string, params: Record = {}): Promise => { +/** + * Fetches a JSON Web Token (JWT) from a remote server. + * + * @param baseUrl - The base URL of the server from which the JWT will be fetched + * @param params - Optional query parameters to be included in the request + * + * @returns A promise that resolves to a string containing the JWT access token if the login is successful + * + * @throws Will throw an error if the fetch operation fails, if the server response is invalid or unexpected, + * or if the login attempt fails. + */ +const fetchJWT = async(baseUrl: string, params?: Record): Promise => { const url = createUrl(baseUrl, 'rsc/jwt', params); + let response: Response; try { - const response = await fetch(url); - if (!response.ok) { - throw new Error('Failed to fetch JWT for current user.'); - } - const result = await response.json() as MCRJWTResponse; - if (!result.login_success) { - throw new Error('Login failed.'); - } - return result.access_token; + response = await fetch(url); } catch (error) { - console.error('Error fetching the token:', error); - throw error; + return handleError('Error while fetching JWT', error); + } + if (!response.ok) { + throw new Error('Failed to fetch JWT for current user.'); + } + const result: MCRJWTResponse = await response.json() as MCRJWTResponse; + if (!result.login_success) { + throw new Error('Login failed.'); } + return result.access_token; }; export { fetchJWT }; diff --git a/src/common/cache/cache.ts b/src/common/cache/cache.ts index 23150ea..585d295 100644 --- a/src/common/cache/cache.ts +++ b/src/common/cache/cache.ts @@ -16,12 +16,55 @@ * along with MyCoRe. If not, see . */ +/** + * A generic interface representing a cache with basic CRUD operations and TTL support. + * + * @typeParam T - The type of the cached values. This can be any type, such as `string`, `number`, or more complex objects + */ interface MCRCache { + + /** + * Sets a value in the cache with an optional time-to-live (TTL) in seconds. + * + * @param key - The key to associate with the value + * @param value - The value to store in the cache + * @param ttl - Optional time-to-live for the cache entry in seconds + */ set(key: string, value: T, ttl?: number): void; + + /** + * Retrieves a value from the cache by its key. + * + * @param key - The key of the cached value to retrieve + * @returns The cached value if it exists, or `undefined` if the key does not exist in the cache + */ get(key: string): T | undefined; + + /** + * Checks if a key exists in the cache. + * + * @param key - The key to check in the cache + * @returns `true` if the key exists in the cache, `false` otherwise + */ has(key: string): boolean; + + /** + * Deletes a value from the cache by its key. + * + * @param key - The key of the cached value to delete + */ delete(key: string): void; + + /** + * Clears all values from the cache. + */ clear(): void; + + /** + * Gets the number of items currently in the cache. + * + * @returns The number of cached items + */ size(): number; } diff --git a/src/common/cache/memory-cache.ts b/src/common/cache/memory-cache.ts index 5563cc4..028e4dc 100644 --- a/src/common/cache/memory-cache.ts +++ b/src/common/cache/memory-cache.ts @@ -18,6 +18,11 @@ import { MCRCache } from './cache'; +/** + * A memory-based cache implementation that stores values in memory using a `Map` with optional TTL (Time-to-Live). + * + * @typeParam T - The type of the cached values. This can be any type, such as `string`, `number`, or more complex objects. + */ class MCRMemoryCache implements MCRCache { private cache: Map; @@ -25,11 +30,17 @@ class MCRMemoryCache implements MCRCache { this.cache = new Map(); } + /** + * @override + */ public set(key: string, value: T, ttl = 0): void { const expiry = ttl === 0 ? null : Date.now() + ttl * 1000; this.cache.set(key, { value, expiry }); } + /** + * @override + */ public get(key: string): T | undefined { const cached = this.cache.get(key); if (!cached) return undefined; @@ -40,6 +51,9 @@ class MCRMemoryCache implements MCRCache { return cached.value; } + /** + * @override + */ public has(key: string): boolean { const cached = this.cache.get(key); if (!cached) return false; @@ -50,14 +64,23 @@ class MCRMemoryCache implements MCRCache { return true; } + /** + * @override + */ public delete(key: string): void { this.cache.delete(key); } + /** + * @override + */ public clear(): void { this.cache.clear(); } + /** + * @override + */ public size(): number { return this.cache.size; } diff --git a/src/common/error.ts b/src/common/error.ts index bbb7f09..1502508 100644 --- a/src/common/error.ts +++ b/src/common/error.ts @@ -16,9 +16,16 @@ * along with MyCoRe. If not, see . */ +/** + * Handles an error by creating a new error message and throwing an error. + * + * @param message - A custom error message to prepend to the error + * @param error - The error object to handle. If the provided `error` is not an instance of `Error`, a generic message ('Unknown error') will be used + * + * @throws Always throws an `Error` with a detailed message, including the provided message and the error's message. + */ const handleError = (message: string, error: unknown): never => { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - throw new Error(`${message}: ${errorMessage}`); + throw new Error(`${message}: ${error instanceof Error ? error.message : 'Unknown error'}`); }; export { handleError }; diff --git a/src/common/url.ts b/src/common/url.ts index e6c4929..73d5190 100644 --- a/src/common/url.ts +++ b/src/common/url.ts @@ -18,24 +18,39 @@ import { handleError } from './error'; +/** + * Creates a `URL` object by combining a base URL, a path, query parameters and a fragment. + * + * @param baseUrl - The base URL, either as a `URL` object or a string + * @param path - An optional path to append to the base URL. Defaults to an empty string + * @param queryParams - An optional object containing key-value pairs for query parameters + * @param fragment - An optional fragment identifier (the part after `#`) to append to the URL + * + * @returns A `URL` object representing the constructed URL. + * + * @throws Will throw an error if the `baseUrl` or `path` is invalid. + */ const createUrl = ( baseUrl: URL | string, - path = '', - queryParams: Record = {}, - fragment = '' + path?: string, + queryParams?: Record, + fragment?: string ): URL => { + let url; try { - const url = new URL(path, baseUrl); + url = (path) ? new URL(path, baseUrl) : new URL (baseUrl); + } catch (error) { + return handleError('Invalid URL input', error); + } + if (queryParams) { Object.entries(queryParams).forEach(([key, value]) => { url.searchParams.set(key, value.toString()); }); - if (fragment) { - url.hash = `#${fragment}`; - } - return url; - } catch (error) { - return handleError('Error while creating the URL', error); } + if (fragment) { + url.hash = `#${fragment}`; + } + return url; }; export { createUrl }; diff --git a/src/i18n/lang-service-impl.ts b/src/i18n/lang-service-impl.ts index 622e3f8..1dd56d1 100644 --- a/src/i18n/lang-service-impl.ts +++ b/src/i18n/lang-service-impl.ts @@ -20,27 +20,50 @@ import { MCRLangService } from './lang-service'; import { MCRCache } from '../common/cache/cache'; import { MCRMemoryCache } from '../common/cache/memory-cache'; +/** + * Implementation of the `MCRLangService` interface that provides language translation functionality. + * + * This class uses a specified base URL to fetch translations, and stores them in a cache for better performance. + * The class supports an optional in-memory cache implementation, but it can also accept any custom cache that implements the `MCRCache` interface. + */ class MCRLangServiceImpl implements MCRLangService { - private baseUrl: string; + private baseUrl: URL; private lang: string; private cache: MCRCache; + /** + * Creates an instance of the `MCRLangServiceImpl` class. + * + * The constructor accepts a base URL, language, and an optional cache implementation. If no cache is provided, + * an in-memory cache (`MCRMemoryCache`) will be used by default. + * + * @param baseUrl - The base URL for the translation service + * @param lang - The language code (e.g., 'en', 'de', etc.) used for translations + * @param cache - An optional cache implementation that adheres to the `MCRCache` interface + */ constructor( - baseUrl: string, + baseUrl: URL | string, lang: string, cache: MCRCache = new MCRMemoryCache() ) { - this.baseUrl = baseUrl; + this.baseUrl = new URL(baseUrl); this.lang = lang; this.cache = cache; } - public fetchTranslations = async (prefix: string): Promise => { + /** + * Fetches and caches translation data for a given prefix and stores it in the cache. + * + * @param prefix - The prefix used to fetch the translations + * @throws If the translation fetch operation fails, an error is thrown indicating the failure and the prefix. + * @returns A promise that resolves once the translations have been fetched and stored in the cache. + */ + public cacheTranslations = async (prefix: string): Promise => { let translationData: Record; try { - translationData = await this.doFetchTranslations(prefix); + translationData = await this.fetchTranslations(prefix); } catch (error) { throw new Error(`Failed to fetch properties for prefix ${prefix}: ${error as string}`); } @@ -49,6 +72,9 @@ class MCRLangServiceImpl implements MCRLangService { }); }; + /** + * @override + */ public translate = async ( key: string, params?: Record @@ -58,7 +84,7 @@ class MCRLangServiceImpl implements MCRLangService { translation = this.cache.get(key); } else { try { - const translationData = await this.doFetchTranslations(key); + const translationData = await this.fetchTranslations(key); if (translationData[key]) { translation = key; this.cache.set(this.getLangKey(key), translationData[key]); @@ -76,10 +102,18 @@ class MCRLangServiceImpl implements MCRLangService { return this.replacePlaceholders(translation, params); }; + /** + * @override + */ public get currentLang(): string { return this.lang; }; + /** + * Sets the current language for translations. + * + * @param newLang - The language code (e.g., 'en', 'de', etc.) to be set as the current language. + */ public set currentLang(newLang: string) { this.lang = newLang; }; @@ -93,20 +127,17 @@ class MCRLangServiceImpl implements MCRLangService { }); }; - private doFetchTranslations = async (prefix: string): Promise> => { + private fetchTranslations = async (prefix: string): Promise> => { + let response: Response; try { - const response = await fetch( - `${this.baseUrl}rsc/locale/translate/${this.lang}/${prefix}`, - ); - if (!response.ok) { - throw new Error( - `Failed to fetch properties from ${prefix}. Status: ${String(response.status)}`, - ); - } - return await response.json() as Record; + response = await fetch(new URL(`rsc/locale/translate/${this.lang}/${prefix}`, this.baseUrl)); } catch { throw new Error('Network error occurred'); } + if (!response.ok) { + throw new Error(`Failed to fetch props for ${prefix}. Status: ${String(response.status)}`); + } + return await response.json() as Record; }; private getLangKey = (key: string): string => { diff --git a/src/i18n/lang-service.ts b/src/i18n/lang-service.ts index 0bfcd45..409b9be 100644 --- a/src/i18n/lang-service.ts +++ b/src/i18n/lang-service.ts @@ -16,8 +16,29 @@ * along with MyCoRe. If not, see . */ +/** + * Interface for a language service that provides translation functionality. + */ interface MCRLangService { + + /** + * Translates a given key into the corresponding language string. + * + * The translation may include placeholders (e.g., `{name}`) which can be replaced by values + * provided in the `params` object. If `params` is not provided, an empty object is used. + * + * @param key - The key representing the string to be translated. + * @param params - A optional record of parameters to replace placeholders in the translation. + * @returns A promise that resolves to the translated string. + */ translate(key: string, params?: Record): Promise; + + /** + * Gets the currently selected language code for the translations. + * + * @returns The language code representing the current language for translations + */ + get currentLang(): string; } export { MCRLangService }; diff --git a/src/orcid/orcid-oauth.ts b/src/orcid/orcid-oauth.ts index d2c0e65..ba58579 100644 --- a/src/orcid/orcid-oauth.ts +++ b/src/orcid/orcid-oauth.ts @@ -19,26 +19,39 @@ import { handleError } from '../common/error'; import { createUrl } from '../common/url'; -const getOrcidOAuthInitUrl = (baseUrl: string, scope?: string): URL => { - try { - if (scope) { - return createUrl(baseUrl, 'rsc/orcid/oauth/init', { 'scope': scope }); - } - return createUrl(baseUrl, 'rsc/orcid/oauth/init'); - } catch (error) { - return handleError('Failed to create Orcid OAuth init URL:', error); +/** + * Generates the URL for initializing the ORCID OAuth process. + * + * @param baseUrl - The base URL for the ORCID OAuth initiation URL. + * @param scope - An optional scope parameter that defines the level of access requested during OAuth authentication + * @returns The constructed URL for initiating the ORCID OAuth process + */ +const getOrcidOAuthInitUrl = (baseUrl: URL | string, scope?: string): URL => { + if (scope) { + return createUrl(baseUrl, 'rsc/orcid/oauth/init', { 'scope': scope }); } + return createUrl(baseUrl, 'rsc/orcid/oauth/init'); }; -const revokeOrcidOAuth = async (baseUrl: string, orcid: string): Promise => { +// TODO add access token +/** + * Revokes the OAuth authorization for a given ORCID. + * + * @param baseUrl - The base URL to which the revoke request is sent + * @param orcid - The ORCID of the user whose OAuth authorization is to be revoked + * @throws If the fetch operation fails or if the response indicates failure + * @returns A promise that resolves when the revoke operation is completed successfully + */ +const revokeOrcidOAuth = async (baseUrl: URL | string, orcid: string): Promise => { + const url = createUrl(baseUrl, `rsc/orcid/oauth/${orcid}`); + let response: Response; try { - const url = createUrl(baseUrl, `rsc/orcid/oauth/${orcid}`); - const response = await fetch(url, { method: 'DELETE' }); - if (!response.ok) { - throw new Error(`Failed to revoke ORCID OAuth: ${String(response.status)}`); - } + response = await fetch(url, { method: 'DELETE' }); } catch (error) { - handleError(`Error during fetch for ORCID revoke: ${orcid}`, error); + return handleError(`Error during fetch for ORCID revoke: ${orcid}`, error); + } + if (!response.ok) { + throw new Error(`Failed to revoke ORCID OAuth: ${String(response.status)}`); } }; diff --git a/src/orcid/orcid-user.ts b/src/orcid/orcid-user.ts index a3cb774..b6ea754 100644 --- a/src/orcid/orcid-user.ts +++ b/src/orcid/orcid-user.ts @@ -18,65 +18,142 @@ import { handleError } from '../common/error'; +/** + * Interface representing the status of an ORCID user. + */ interface MCROrcidUserStatus { + + /** + * A list of ORCIDs associated with the user. + */ orcids: string[]; + + /** + * A list of trusted ORCIDs associated with the user. + */ trustedOrcids: string[]; } +/** + * Interface representing the settings for an ORCID user. + */ interface MCROrcidUserSettings { + /** + * A flag indicating if works should always be updated. + * `null` means that no preference has been set. + */ isAlwaysUpdateWork: boolean | null; + + /** + * A flag indicating if duplicate works should be created. + * `null` means that no preference has been set. + */ isCreateDuplicateWork: boolean | null; + + /** + * A flag indicating if the first work should be created. + * `null` means that no preference has been set. + */ isCreateFirstWork: boolean | null; + + /** + * A flag indicating if deleted works should be recreated. + * `null` means that no preference has been set. + */ isRecreateDeletedWork: boolean | null; } +/** + * Service for interacting with ORCID user status and settings. + */ class MCROrcidUserService { private baseUrl: URL; - private accessToken: string; - - constructor(baseUrl: URL | string, bearerAccessToken: string) { + /** + * Creates an instance of the MCROrcidUserService. + * + * @param baseUrl - The base URL to be used for making API requests to the ORCID service. + */ + constructor(baseUrl: URL) { this.baseUrl = new URL(baseUrl); - this.accessToken = bearerAccessToken; } - public fetchOrcidUserStatus = async (): Promise => { + /** + * Fetches the status of the ORCID user. + * + * @param accessToken - The access token to authorize the request + * @returns A promise that resolves to an `MCROrcidUserStatus` object containing the user status + * @throws If the fetch operation fails or if the response is not successful + */ + public fetchOrcidUserStatus = async (accessToken: string): Promise => { + let response: Response; try { - const response = await fetch(new URL('api/orcid/v1/user-status', this.baseUrl), { - headers: { Authorization: `Bearer ${this.accessToken}`} + response = await fetch(new URL('api/orcid/v1/user-status', this.baseUrl), { + headers: { Authorization: `Bearer ${accessToken}`} }); - if (!response.ok) { - throw new Error(`Failed to fetch Orcid user status: ${String(response.status)}`); - } - return await response.json() as MCROrcidUserStatus; } catch (error) { - return handleError('An error occurred while fetching Orcid user status', error); + return handleError('Error while fetching Orcid user status', error); } + if (!response.ok) { + throw new Error(`Failed to fetch Orcid user status: ${String(response.status)}`); + } + return await response.json() as MCROrcidUserStatus; }; - public fetchOrcidUserSettings = async (orcid: string): Promise => { - const response = await fetch(`${this.baseUrl}api/orcid/v1/user-properties/${orcid}`, { - headers: { Authorization: `Bearer ${this.accessToken}`}, - }); + /** + * Fetches the settings for an ORCID user. + * + * @param accessToken - The access token to authorize the request + * @param orcid - The ORCID of the user whose settings are being fetched + * @returns A promise that resolves to an `MCROrcidUserSettings` object containing the user's settings + * @throws If the fetch operation fails or if the response is not successful + */ + public fetchOrcidUserSettings = async ( + accessToken: string, + orcid: string + ): Promise => { + let response: Response; + try { + response = await fetch(`${this.baseUrl}api/orcid/v1/user-properties/${orcid}`, { + headers: { Authorization: `Bearer ${accessToken}`}, + }); + } catch (error) { + return handleError('Error while fetching Orcid user settings', error); + } if (!response.ok) { throw new Error(`Failed to fetch ORCID user settings for ${orcid}.`); } return await response.json() as MCROrcidUserSettings; }; + /** + * Updates the settings for an ORCID user. + * + * @param accessToken - The access token to authorize the request + * @param orcid - The ORCID of the user whose settings are being updated + * @param settings - An object containing the new settings for the user + * @returns A promise that resolves when the settings are successfully updated + * @throws If the fetch operation fails or if the response is not successful + */ public updateOrcidUserSettings = async ( + accessToken: string, orcid: string, settings: MCROrcidUserSettings ): Promise => { - const response = await fetch(`${this.baseUrl}api/orcid/v1/user-properties/${orcid}`, { - method: 'PUT', - headers: { - Authorization: `Bearer ${this.accessToken}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify(settings), - }); + let response: Response; + try { + response = await fetch(`${this.baseUrl}api/orcid/v1/user-properties/${orcid}`, { + method: 'PUT', + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(settings), + }); + } catch (error) { + return handleError('Error while fetching Orcid user settings', error); + } if (!response.ok) { throw new Error(`Failed to update ORCID user settings for ${orcid}.`); } diff --git a/src/orcid/orcid-work.ts b/src/orcid/orcid-work.ts index e426f03..2a3444b 100644 --- a/src/orcid/orcid-work.ts +++ b/src/orcid/orcid-work.ts @@ -16,44 +16,110 @@ * along with MyCoRe. If not, see . */ +import { handleError } from '../common/error'; + +/** + * Interface representing the status of an ORCID work. + * + * The `MCROrcidWorkStatus` interface describes the status of a work, including whether the work + * belongs to the user (`own`) and any other associated works (`other`). + */ interface MCROrcidWorkStatus { + + /** + * The put code string of the work owned by the user. + * If no status is available, this will be `null`. + */ own: string | null; + + /** + * A list of put code string for other works associated with the user. + */ other: string[]; } +/** + * A service for interacting with ORCID works, including fetching work status and exporting objects. + * + * This service allows you to fetch the status of a work by its `objectId` and ORCID, and to export + * works to ORCID. It can operate in both "member" and "public" modes. + */ class MCROrcidWorkService { - private baseUrl: string; - - private accessToken: string; + private baseUrl: URL; - constructor(baseUrl: string, accessToken: string) { - this.baseUrl = baseUrl; - this.accessToken = accessToken; + /** + * Creates an instance of the MCROrcidWorkService. + * + * @param baseUrl - The base URL to be used for making API requests to the ORCID service. + */ + constructor(baseUrl: URL | string) { + this.baseUrl = new URL(baseUrl); } + /** + * Fetches the status of a work for a specific ORCID and object ID. + * + * This method fetches the status of a work (owned by the user or other associated works) using + * the provided access token, ORCID, and object ID. It can operate in "member" or "public" mode, + * depending on the `useMember` flag. + * + * @param accessToken - The access token to authorize the request + * @param orcid - The ORCID of the user for whom the work status is to be fetched + * @param objectId - The object ID of the work whose status is being requested + * @param useMember - A boolean flag indicating whether to fetch in "member" mode (`true`) or "public" mode (`false`) + * @returns A promise that resolves to an `MCROrcidWorkStatus` object containing the status of the work + * @throws If the fetch operation fails or if the response is not successful + */ public fetchWorkStatus = async ( + accessToken: string, orcid: string, objectId: string, useMember = false ): Promise => { const mode = useMember ? 'member' : 'public'; - const url = `${this.baseUrl}api/orcid/v1/${mode}/${orcid}/works/object/${objectId}`; - const response = await fetch(url, { - headers: { Authorization: `Bearer ${this.accessToken}`} - }); + const url = new URL(`api/orcid/v1/${mode}/${orcid}/works/object/${objectId}`, this.baseUrl); + let response: Response; + try { + response = await fetch(url, { + headers: { Authorization: `Bearer ${accessToken}`} + }); + } catch (error) { + return handleError('Failed to fetch work status', error); + } if (!response.ok) { throw new Error(`Failed to fetch work status for ${objectId}.`); } return await response.json() as MCROrcidWorkStatus; }; - public exportObjectToOrcid = async (orcid: string, objectId: string): Promise => { - const url = `${this.baseUrl}api/orcid/v1/member/${orcid}/works/object/${objectId}`; - const response = await fetch(url, { - method: 'POST', - headers: { Authorization: `Bearer ${this.accessToken}`}, - }); + /** + * Exports an object to ORCID for a specific user and object ID. + * + * This method sends a POST request to export the specified object to ORCID for the provided ORCID. + * It requires an OAuth access token for authorization. + * + * @param accessToken - The access token to authorize the request + * @param orcid - The ORCID of the user to whom the object should be exported + * @param objectId - The object ID of the work to be exported + * @returns A promise that resolves when the export operation is completed + * @throws If the fetch operation fails or if the response is not successful + */ + public exportObjectToOrcid = async ( + accessToken: string, + orcid: string, + objectId: string + ): Promise => { + const url = new URL(`api/orcid/v1/member/${orcid}/works/object/${objectId}`, this.baseUrl); + let response: Response; + try { + response = await fetch(url, { + method: 'POST', + headers: { Authorization: `Bearer ${accessToken}`}, + }); + } catch (error) { + return handleError('Failed to request export', error); + } if (!response.ok) { throw new Error(`Failed to export ${objectId} to ${orcid}.`); }