mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 22:41:09 +02:00
Merge branch 'dev-1' into dev
This commit is contained in:
commit
d41d060bb3
1770 changed files with 168028 additions and 31031 deletions
17
packages/shared-api-client/package.json
Normal file
17
packages/shared-api-client/package.json
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"name": "@manacore/shared-api-client",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
}
|
||||
218
packages/shared-api-client/src/client.ts
Normal file
218
packages/shared-api-client/src/client.ts
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
/**
|
||||
* Shared API Client Factory
|
||||
* Creates a configured API client for making authenticated requests.
|
||||
*/
|
||||
|
||||
import type { ApiResponse, FetchOptions } from './types';
|
||||
|
||||
export interface ApiClientConfig {
|
||||
/** Base URL for the API (e.g., 'http://localhost:3002') */
|
||||
baseUrl: string;
|
||||
/** Optional API prefix (default: '/api') */
|
||||
apiPrefix?: string;
|
||||
/** Function to get the current auth token */
|
||||
getToken?: () => Promise<string | null> | string | null;
|
||||
/** Whether running in browser environment */
|
||||
isBrowser?: boolean;
|
||||
/** Local storage key for token fallback */
|
||||
tokenStorageKey?: string;
|
||||
}
|
||||
|
||||
export interface ApiClient {
|
||||
/** Make a GET request */
|
||||
get: <T>(endpoint: string, options?: Omit<FetchOptions, 'method'>) => Promise<ApiResponse<T>>;
|
||||
/** Make a POST request */
|
||||
post: <T>(
|
||||
endpoint: string,
|
||||
body?: unknown,
|
||||
options?: Omit<FetchOptions, 'method' | 'body'>
|
||||
) => Promise<ApiResponse<T>>;
|
||||
/** Make a PUT request */
|
||||
put: <T>(
|
||||
endpoint: string,
|
||||
body?: unknown,
|
||||
options?: Omit<FetchOptions, 'method' | 'body'>
|
||||
) => Promise<ApiResponse<T>>;
|
||||
/** Make a PATCH request */
|
||||
patch: <T>(
|
||||
endpoint: string,
|
||||
body?: unknown,
|
||||
options?: Omit<FetchOptions, 'method' | 'body'>
|
||||
) => Promise<ApiResponse<T>>;
|
||||
/** Make a DELETE request */
|
||||
delete: <T>(endpoint: string, options?: Omit<FetchOptions, 'method'>) => Promise<ApiResponse<T>>;
|
||||
/** Make a request with any method */
|
||||
request: <T>(endpoint: string, options?: FetchOptions) => Promise<ApiResponse<T>>;
|
||||
/** Upload a single file */
|
||||
uploadFile: <T>(endpoint: string, file: File, token?: string) => Promise<ApiResponse<T>>;
|
||||
/** Upload multiple files */
|
||||
uploadFiles: <T>(endpoint: string, files: File[], token?: string) => Promise<ApiResponse<T>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an API client with the given configuration.
|
||||
*/
|
||||
export function createApiClient(config: ApiClientConfig): ApiClient {
|
||||
const { baseUrl, apiPrefix = '/api', getToken, isBrowser = true, tokenStorageKey } = config;
|
||||
|
||||
async function getAuthToken(providedToken?: string): Promise<string | undefined> {
|
||||
if (providedToken) return providedToken;
|
||||
|
||||
if (getToken) {
|
||||
const token = await getToken();
|
||||
if (token) return token;
|
||||
}
|
||||
|
||||
// Fallback to localStorage if in browser and key provided
|
||||
if (isBrowser && tokenStorageKey && typeof localStorage !== 'undefined') {
|
||||
return localStorage.getItem(tokenStorageKey) || undefined;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async function request<T>(endpoint: string, options: FetchOptions = {}): Promise<ApiResponse<T>> {
|
||||
const { method = 'GET', body, token, isFormData = false, headers: customHeaders } = options;
|
||||
|
||||
const authToken = await getAuthToken(token);
|
||||
|
||||
try {
|
||||
const headers: Record<string, string> = { ...customHeaders };
|
||||
|
||||
// Don't set Content-Type for FormData - browser sets it automatically with boundary
|
||||
if (!isFormData) {
|
||||
headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
|
||||
if (authToken) {
|
||||
headers['Authorization'] = `Bearer ${authToken}`;
|
||||
}
|
||||
|
||||
const url = `${baseUrl}${apiPrefix}${endpoint}`;
|
||||
const response = await fetch(url, {
|
||||
method,
|
||||
headers,
|
||||
body: isFormData ? (body as FormData) : body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
return {
|
||||
data: null,
|
||||
error: new Error(errorData.message || `API error: ${response.status}`),
|
||||
};
|
||||
}
|
||||
|
||||
// Handle empty responses (204 No Content)
|
||||
if (response.status === 204) {
|
||||
return { data: null, error: null };
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return { data, error: null };
|
||||
} catch (error) {
|
||||
return {
|
||||
data: null,
|
||||
error: error instanceof Error ? error : new Error('Unknown error'),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadFile<T>(
|
||||
endpoint: string,
|
||||
file: File,
|
||||
token?: string
|
||||
): Promise<ApiResponse<T>> {
|
||||
const authToken = await getAuthToken(token);
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
const headers: Record<string, string> = {};
|
||||
if (authToken) {
|
||||
headers['Authorization'] = `Bearer ${authToken}`;
|
||||
}
|
||||
|
||||
const response = await fetch(`${baseUrl}${apiPrefix}${endpoint}`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
return {
|
||||
data: null,
|
||||
error: new Error(errorData.message || `Upload error: ${response.status}`),
|
||||
};
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return { data, error: null };
|
||||
} catch (error) {
|
||||
return {
|
||||
data: null,
|
||||
error: error instanceof Error ? error : new Error('Upload failed'),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadFiles<T>(
|
||||
endpoint: string,
|
||||
files: File[],
|
||||
token?: string
|
||||
): Promise<ApiResponse<T>> {
|
||||
const authToken = await getAuthToken(token);
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
files.forEach((file) => {
|
||||
formData.append('files', file);
|
||||
});
|
||||
|
||||
const headers: Record<string, string> = {};
|
||||
if (authToken) {
|
||||
headers['Authorization'] = `Bearer ${authToken}`;
|
||||
}
|
||||
|
||||
const response = await fetch(`${baseUrl}${apiPrefix}${endpoint}`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
return {
|
||||
data: null,
|
||||
error: new Error(errorData.message || `Upload error: ${response.status}`),
|
||||
};
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return { data, error: null };
|
||||
} catch (error) {
|
||||
return {
|
||||
data: null,
|
||||
error: error instanceof Error ? error : new Error('Upload failed'),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
get: <T>(endpoint: string, options?: Omit<FetchOptions, 'method'>) =>
|
||||
request<T>(endpoint, { ...options, method: 'GET' }),
|
||||
post: <T>(endpoint: string, body?: unknown, options?: Omit<FetchOptions, 'method' | 'body'>) =>
|
||||
request<T>(endpoint, { ...options, method: 'POST', body }),
|
||||
put: <T>(endpoint: string, body?: unknown, options?: Omit<FetchOptions, 'method' | 'body'>) =>
|
||||
request<T>(endpoint, { ...options, method: 'PUT', body }),
|
||||
patch: <T>(endpoint: string, body?: unknown, options?: Omit<FetchOptions, 'method' | 'body'>) =>
|
||||
request<T>(endpoint, { ...options, method: 'PATCH', body }),
|
||||
delete: <T>(endpoint: string, options?: Omit<FetchOptions, 'method'>) =>
|
||||
request<T>(endpoint, { ...options, method: 'DELETE' }),
|
||||
request,
|
||||
uploadFile,
|
||||
uploadFiles,
|
||||
};
|
||||
}
|
||||
7
packages/shared-api-client/src/index.ts
Normal file
7
packages/shared-api-client/src/index.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
/**
|
||||
* Shared API Client for ManaCore Apps
|
||||
* Provides a unified way to make API calls with authentication.
|
||||
*/
|
||||
|
||||
export { createApiClient, type ApiClientConfig, type ApiClient } from './client';
|
||||
export { type ApiResponse, type FetchOptions, type HttpMethod } from './types';
|
||||
18
packages/shared-api-client/src/types.ts
Normal file
18
packages/shared-api-client/src/types.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
/**
|
||||
* Shared API Client Types
|
||||
*/
|
||||
|
||||
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
||||
|
||||
export interface FetchOptions {
|
||||
method?: HttpMethod;
|
||||
body?: unknown;
|
||||
token?: string;
|
||||
isFormData?: boolean;
|
||||
headers?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface ApiResponse<T> {
|
||||
data: T | null;
|
||||
error: Error | null;
|
||||
}
|
||||
16
packages/shared-api-client/tsconfig.json
Normal file
16
packages/shared-api-client/tsconfig.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue