diff --git a/src/auth/fetch-jwt.ts b/src/auth/fetch-jwt.ts new file mode 100644 index 0000000..02c7a50 --- /dev/null +++ b/src/auth/fetch-jwt.ts @@ -0,0 +1,56 @@ +/*! + * This file is part of *** M y C o R e *** + * See https://www.mycore.de/ for details. + * + * MyCoRe is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * MyCoRe is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MyCoRe. If not, see . + */ + +import { createUrl } from '../common/url'; +import { handleError } from '../common/error'; + +interface MCRJWTResponse { + login_success: boolean; + access_token: string; +} + +/** + * 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 { + response = await fetch(url); + } catch (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/auth/index.ts b/src/auth/index.ts new file mode 100644 index 0000000..fcd066b --- /dev/null +++ b/src/auth/index.ts @@ -0,0 +1 @@ +export * from './fetch-jwt'; diff --git a/src/common/cache/cache.ts b/src/common/cache/cache.ts new file mode 100644 index 0000000..585d295 --- /dev/null +++ b/src/common/cache/cache.ts @@ -0,0 +1,71 @@ +/* + * This file is part of *** M y C o R e *** + * See https://www.mycore.de/ for details. + * + * MyCoRe is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * MyCoRe is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * 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; +} + +export { MCRCache }; diff --git a/src/common/cache/index.ts b/src/common/cache/index.ts new file mode 100644 index 0000000..22a759d --- /dev/null +++ b/src/common/cache/index.ts @@ -0,0 +1,2 @@ +export * from './cache'; +export * from './memory-cache'; diff --git a/src/common/cache/memory-cache.ts b/src/common/cache/memory-cache.ts new file mode 100644 index 0000000..028e4dc --- /dev/null +++ b/src/common/cache/memory-cache.ts @@ -0,0 +1,89 @@ +/* + * This file is part of *** M y C o R e *** + * See https://www.mycore.de/ for details. + * + * MyCoRe is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * MyCoRe is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MyCoRe. If not, see . + */ + +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; + + constructor() { + 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; + if (cached.expiry !== null && cached.expiry < Date.now()) { + this.cache.delete(key); + return undefined; + } + return cached.value; + } + + /** + * @override + */ + public has(key: string): boolean { + const cached = this.cache.get(key); + if (!cached) return false; + if (cached.expiry !== null && cached.expiry < Date.now()) { + this.cache.delete(key); + return false; + } + 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; + } +} + +export { MCRMemoryCache }; diff --git a/src/common/error.ts b/src/common/error.ts new file mode 100644 index 0000000..1502508 --- /dev/null +++ b/src/common/error.ts @@ -0,0 +1,31 @@ +/* + * This file is part of *** M y C o R e *** + * See https://www.mycore.de/ for details. + * + * MyCoRe is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * MyCoRe is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * 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 => { + 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 new file mode 100644 index 0000000..73d5190 --- /dev/null +++ b/src/common/url.ts @@ -0,0 +1,56 @@ +/* + * This file is part of *** M y C o R e *** + * See https://www.mycore.de/ for details. + * + * MyCoRe is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * MyCoRe is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MyCoRe. If not, see . + */ + +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?: string, + queryParams?: Record, + fragment?: string +): URL => { + let url; + try { + 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; +}; + +export { createUrl }; diff --git a/src/i18n/index.ts b/src/i18n/index.ts new file mode 100644 index 0000000..5cd0529 --- /dev/null +++ b/src/i18n/index.ts @@ -0,0 +1,3 @@ +export * from './lang-service'; +export * from './lang-service-impl'; + diff --git a/src/i18n/lang-service-impl.ts b/src/i18n/lang-service-impl.ts new file mode 100644 index 0000000..1dd56d1 --- /dev/null +++ b/src/i18n/lang-service-impl.ts @@ -0,0 +1,148 @@ +/* + * This file is part of *** M y C o R e *** + * See https://www.mycore.de/ for details. + * + * MyCoRe is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * MyCoRe is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MyCoRe. If not, see . + */ + +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: 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: URL | string, + lang: string, + cache: MCRCache = new MCRMemoryCache() + ) { + this.baseUrl = new URL(baseUrl); + this.lang = lang; + this.cache = cache; + } + + /** + * 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.fetchTranslations(prefix); + } catch (error) { + throw new Error(`Failed to fetch properties for prefix ${prefix}: ${error as string}`); + } + Object.entries(translationData).forEach(([key, translation]: [string, string]) => { + this.cache.set(this.getLangKey(key), translation); + }); + }; + + /** + * @override + */ + public translate = async ( + key: string, + params?: Record + ): Promise => { + let translation = null; + if (this.cache.has(this.getLangKey(key))) { + translation = this.cache.get(key); + } else { + try { + const translationData = await this.fetchTranslations(key); + if (translationData[key]) { + translation = key; + this.cache.set(this.getLangKey(key), translationData[key]); + } + } catch (error) { + console.error(`Error while fetching key ${key}`, error); + } + } + if (!translation) { + return `??${key}??`; + } + if (!params) { + return translation; + } + 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; + }; + + private replacePlaceholders(translation: string, params: Record) { + return translation.replace(/{(\w+)}/g, (match: string, placeholder: string) => { + if (placeholder in params) { + return String(params[placeholder] ?? match); + } + return match; + }); + }; + + private fetchTranslations = async (prefix: string): Promise> => { + let response: Response; + try { + 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 => { + return `${this.lang}_${key}`; + }; +} + +export { MCRLangServiceImpl }; diff --git a/src/i18n/lang-service.ts b/src/i18n/lang-service.ts new file mode 100644 index 0000000..409b9be --- /dev/null +++ b/src/i18n/lang-service.ts @@ -0,0 +1,44 @@ +/* + * This file is part of *** M y C o R e *** + * See https://www.mycore.de/ for details. + * + * MyCoRe is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * MyCoRe is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * 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/index.ts b/src/orcid/index.ts new file mode 100644 index 0000000..e010919 --- /dev/null +++ b/src/orcid/index.ts @@ -0,0 +1,3 @@ +export * from './orcid-user.ts'; +export * from './orcid-work.ts'; +export * from './orcid-oauth'; diff --git a/src/orcid/orcid-oauth.ts b/src/orcid/orcid-oauth.ts new file mode 100644 index 0000000..ba58579 --- /dev/null +++ b/src/orcid/orcid-oauth.ts @@ -0,0 +1,58 @@ +/*! + * This file is part of *** M y C o R e *** + * See https://www.mycore.de/ for details. + * + * MyCoRe is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * MyCoRe is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MyCoRe. If not, see . + */ + +import { handleError } from '../common/error'; +import { createUrl } from '../common/url'; + +/** + * 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'); +}; + +// 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 { + response = await fetch(url, { method: 'DELETE' }); + } catch (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)}`); + } +}; + +export { getOrcidOAuthInitUrl, revokeOrcidOAuth }; diff --git a/src/orcid/orcid-user.ts b/src/orcid/orcid-user.ts new file mode 100644 index 0000000..b6ea754 --- /dev/null +++ b/src/orcid/orcid-user.ts @@ -0,0 +1,163 @@ +/*! + * This file is part of *** M y C o R e *** + * See https://www.mycore.de/ for details. + * + * MyCoRe is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * MyCoRe is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MyCoRe. If not, see . + */ + +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; + + /** + * 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); + } + + /** + * 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 { + response = await fetch(new URL('api/orcid/v1/user-status', this.baseUrl), { + headers: { Authorization: `Bearer ${accessToken}`} + }); + } catch (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; + }; + + /** + * 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 => { + 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}.`); + } + }; +} + +export { MCROrcidUserService, MCROrcidUserStatus, MCROrcidUserSettings }; diff --git a/src/orcid/orcid-work.ts b/src/orcid/orcid-work.ts new file mode 100644 index 0000000..2a3444b --- /dev/null +++ b/src/orcid/orcid-work.ts @@ -0,0 +1,129 @@ +/* + * This file is part of *** M y C o R e *** + * See https://www.mycore.de/ for details. + * + * MyCoRe is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * MyCoRe is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * 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: URL; + + /** + * 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 = 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; + }; + + /** + * 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}.`); + } + }; +} + +export { MCROrcidWorkStatus, MCROrcidWorkService };