Skip to content

Commit

Permalink
fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
golsch committed Dec 13, 2024
1 parent 3f30a77 commit 22658a5
Show file tree
Hide file tree
Showing 10 changed files with 403 additions and 95 deletions.
36 changes: 24 additions & 12 deletions src/auth/fetch-jwt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> = {}): Promise<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<string, string>): Promise<string> => {
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 };
43 changes: 43 additions & 0 deletions src/common/cache/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,55 @@
* along with MyCoRe. If not, see <http://www.gnu.org/licenses/>.
*/

/**
* 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<T> {

/**
* 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;
}

Expand Down
23 changes: 23 additions & 0 deletions src/common/cache/memory-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,29 @@

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<T> implements MCRCache<T> {
private cache: Map<string, { value: T; expiry: number | null }>;

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;
Expand All @@ -40,6 +51,9 @@ class MCRMemoryCache<T> implements MCRCache<T> {
return cached.value;
}

/**
* @override
*/
public has(key: string): boolean {
const cached = this.cache.get(key);
if (!cached) return false;
Expand All @@ -50,14 +64,23 @@ class MCRMemoryCache<T> implements MCRCache<T> {
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;
}
Expand Down
11 changes: 9 additions & 2 deletions src/common/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,16 @@
* along with MyCoRe. If not, see <http://www.gnu.org/licenses/>.
*/

/**
* 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 };
35 changes: 25 additions & 10 deletions src/common/url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string | number> = {},
fragment = ''
path?: string,
queryParams?: Record<string, string | number>,
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 };
63 changes: 47 additions & 16 deletions src/i18n/lang-service-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>;

/**
* 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<string> = new MCRMemoryCache<string>()
) {
this.baseUrl = baseUrl;
this.baseUrl = new URL(baseUrl);
this.lang = lang;
this.cache = cache;
}

public fetchTranslations = async (prefix: string): Promise<void> => {
/**
* 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<void> => {
let translationData: Record<string, string>;
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}`);
}
Expand All @@ -49,6 +72,9 @@ class MCRLangServiceImpl implements MCRLangService {
});
};

/**
* @override
*/
public translate = async (
key: string,
params?: Record<string, string | number>
Expand All @@ -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]);
Expand All @@ -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;
};
Expand All @@ -93,20 +127,17 @@ class MCRLangServiceImpl implements MCRLangService {
});
};

private doFetchTranslations = async (prefix: string): Promise<Record<string, string>> => {
private fetchTranslations = async (prefix: string): Promise<Record<string, string>> => {
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<string, string>;
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<string, string>;
};

private getLangKey = (key: string): string => {
Expand Down
21 changes: 21 additions & 0 deletions src/i18n/lang-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,29 @@
* along with MyCoRe. If not, see <http://www.gnu.org/licenses/>.
*/

/**
* 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<string, string | number>): Promise<string>;

/**
* Gets the currently selected language code for the translations.
*
* @returns The language code representing the current language for translations
*/
get currentLang(): string;
}

export { MCRLangService };
Loading

0 comments on commit 22658a5

Please sign in to comment.