mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-04-19 10:44:06 +00:00
Merge pull request #1039 from bigcapitalhq/settings-hooks
feat(sdk-ts): settings fetch utils
This commit is contained in:
@@ -1,22 +1,29 @@
|
||||
import { Fetcher } from 'openapi-typescript-fetch';
|
||||
import type { paths } from './schema';
|
||||
import { createCamelCaseMiddleware } from './middleware/camel-case-middleware';
|
||||
|
||||
export type ApiFetcher = ReturnType<typeof Fetcher.for<paths>>;
|
||||
|
||||
export interface CreateApiFetcherConfig {
|
||||
baseUrl?: string;
|
||||
init?: RequestInit;
|
||||
/** Set to true to disable automatic snake_case to camelCase transformation */
|
||||
disableCamelCaseTransform?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and configures an ApiFetcher for use with sdk-ts fetch functions.
|
||||
* Call this with baseUrl (e.g. '/api') and init.headers (Authorization, organization-id, etc.) from the app.
|
||||
*
|
||||
* By default, all JSON response keys are automatically transformed from snake_case to camelCase.
|
||||
* Set disableCamelCaseTransform: true to disable this behavior.
|
||||
*/
|
||||
export function createApiFetcher(config?: CreateApiFetcherConfig): ApiFetcher {
|
||||
const fetcher = Fetcher.for<paths>();
|
||||
fetcher.configure({
|
||||
baseUrl: config?.baseUrl ?? '',
|
||||
init: config?.init,
|
||||
use: config?.disableCamelCaseTransform ? [] : [createCamelCaseMiddleware()],
|
||||
});
|
||||
return fetcher;
|
||||
}
|
||||
@@ -27,3 +34,42 @@ export function createApiFetcher(config?: CreateApiFetcherConfig): ApiFetcher {
|
||||
export function normalizeApiPath(path: string): string {
|
||||
return (path || '').replace(/^\//, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a raw API request using the fetcher's configuration (baseUrl, headers, middleware).
|
||||
* Use this for endpoints not defined in the OpenAPI schema.
|
||||
*/
|
||||
export async function rawRequest<T = unknown>(
|
||||
fetcher: ApiFetcher,
|
||||
method: string,
|
||||
path: string,
|
||||
body?: Record<string, unknown>
|
||||
): Promise<T> {
|
||||
// Access the fetcher's internal configuration
|
||||
const fetcherConfig = (fetcher as unknown as { config?: { baseUrl: string; init?: RequestInit } }).config;
|
||||
const baseUrl = fetcherConfig?.baseUrl ?? '';
|
||||
const init = fetcherConfig?.init ?? {};
|
||||
|
||||
const url = `${baseUrl}${path}`;
|
||||
const headers: Record<string, string> = {
|
||||
'Accept': 'application/json',
|
||||
...(init.headers as Record<string, string> || {}),
|
||||
};
|
||||
|
||||
const requestInit: RequestInit = {
|
||||
...init,
|
||||
method,
|
||||
headers,
|
||||
};
|
||||
|
||||
if (body && (method === 'POST' || method === 'PUT' || method === 'PATCH')) {
|
||||
headers['Content-Type'] = 'application/json';
|
||||
requestInit.body = JSON.stringify(body);
|
||||
}
|
||||
|
||||
const response = await fetch(url, requestInit);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
return response.json() as Promise<T>;
|
||||
}
|
||||
|
||||
28
shared/sdk-ts/src/middleware/camel-case-middleware.ts
Normal file
28
shared/sdk-ts/src/middleware/camel-case-middleware.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Middleware to transform API response keys from snake_case to camelCase.
|
||||
* Automatically applied by createApiFetcher unless explicitly disabled.
|
||||
*/
|
||||
import type { ApiResponse, Middleware } from 'openapi-typescript-fetch';
|
||||
import { transformKeysToCamelCase } from '../utils/case-transform';
|
||||
|
||||
/**
|
||||
* Creates a middleware that transforms all JSON response keys from snake_case to camelCase.
|
||||
* Non-JSON responses (PDF, CSV, XLSX) are passed through unchanged.
|
||||
*/
|
||||
export function createCamelCaseMiddleware(): Middleware {
|
||||
return async (url, init, next): Promise<ApiResponse> => {
|
||||
const response = await next(url, init);
|
||||
|
||||
// Skip transformation for non-JSON content types (PDF, CSV, XLSX, etc.)
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (contentType && !contentType.includes('application/json')) {
|
||||
return response;
|
||||
}
|
||||
|
||||
// Transform response data keys from snake_case to camelCase
|
||||
return {
|
||||
...response,
|
||||
data: transformKeysToCamelCase(response.data),
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { ApiFetcher } from './fetch-utils';
|
||||
import { rawRequest } from './fetch-utils';
|
||||
import { paths } from './schema';
|
||||
import { OpForPath, OpQueryParams, OpRequestBody, OpResponseBody } from './utils';
|
||||
|
||||
@@ -28,3 +29,88 @@ export async function saveSettings(
|
||||
const put = fetcher.path(SETTINGS_ROUTES.GET_SAVE).method('put').create();
|
||||
await put(values);
|
||||
}
|
||||
|
||||
export const editSettings = saveSettings;
|
||||
|
||||
// Settings group fetchers
|
||||
|
||||
export async function fetchSettingsInvoices(
|
||||
fetcher: ApiFetcher
|
||||
): Promise<SettingsResponse> {
|
||||
return fetchSettings(fetcher, { group: 'sale_invoices' } as GetSettingsQuery);
|
||||
}
|
||||
|
||||
export async function fetchSettingsEstimates(
|
||||
fetcher: ApiFetcher
|
||||
): Promise<SettingsResponse> {
|
||||
return fetchSettings(fetcher, { group: 'sale_estimates' } as GetSettingsQuery);
|
||||
}
|
||||
|
||||
export async function fetchSettingsPaymentReceives(
|
||||
fetcher: ApiFetcher
|
||||
): Promise<SettingsResponse> {
|
||||
return fetchSettings(fetcher, { group: 'payment_receives' } as GetSettingsQuery);
|
||||
}
|
||||
|
||||
export async function fetchSettingsReceipts(
|
||||
fetcher: ApiFetcher
|
||||
): Promise<SettingsResponse> {
|
||||
return fetchSettings(fetcher, { group: 'sale_receipts' } as GetSettingsQuery);
|
||||
}
|
||||
|
||||
export async function fetchSettingsManualJournals(
|
||||
fetcher: ApiFetcher
|
||||
): Promise<SettingsResponse> {
|
||||
return fetchSettings(fetcher, { group: 'manual_journals' } as GetSettingsQuery);
|
||||
}
|
||||
|
||||
export async function fetchSettingsItems(
|
||||
fetcher: ApiFetcher
|
||||
): Promise<SettingsResponse> {
|
||||
return fetchSettings(fetcher, { group: 'items' } as GetSettingsQuery);
|
||||
}
|
||||
|
||||
export async function fetchSettingCashFlow(
|
||||
fetcher: ApiFetcher
|
||||
): Promise<SettingsResponse> {
|
||||
return fetchSettings(fetcher, { group: 'cashflow' } as GetSettingsQuery);
|
||||
}
|
||||
|
||||
export async function fetchSettingsCreditNotes(
|
||||
fetcher: ApiFetcher
|
||||
): Promise<SettingsResponse> {
|
||||
return fetchSettings(fetcher, { group: 'credit_note' } as GetSettingsQuery);
|
||||
}
|
||||
|
||||
export async function fetchSettingsVendorCredits(
|
||||
fetcher: ApiFetcher
|
||||
): Promise<SettingsResponse> {
|
||||
return fetchSettings(fetcher, { group: 'vendor_credit' } as GetSettingsQuery);
|
||||
}
|
||||
|
||||
export async function fetchSettingsWarehouseTransfers(
|
||||
fetcher: ApiFetcher
|
||||
): Promise<SettingsResponse> {
|
||||
return fetchSettings(fetcher, { group: 'warehouse_transfers' } as GetSettingsQuery);
|
||||
}
|
||||
|
||||
// SMS Notification settings (using raw fetch since endpoints are not in OpenAPI schema)
|
||||
|
||||
export async function fetchSettingSMSNotifications(fetcher: ApiFetcher): Promise<unknown> {
|
||||
return rawRequest(fetcher, 'GET', '/api/settings/sms-notifications');
|
||||
}
|
||||
|
||||
export async function fetchSettingSMSNotification(
|
||||
fetcher: ApiFetcher,
|
||||
key: string
|
||||
): Promise<unknown> {
|
||||
return rawRequest(fetcher, 'GET', `/api/settings/sms-notification/${encodeURIComponent(key)}`);
|
||||
}
|
||||
|
||||
export async function editSettingSMSNotification(
|
||||
fetcher: ApiFetcher,
|
||||
key: string,
|
||||
values: Record<string, unknown>
|
||||
): Promise<unknown> {
|
||||
return rawRequest(fetcher, 'POST', '/api/settings/sms-notification', { key, ...values });
|
||||
}
|
||||
|
||||
76
shared/sdk-ts/src/utils/case-transform.ts
Normal file
76
shared/sdk-ts/src/utils/case-transform.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* Utilities for transforming object keys from snake_case to camelCase.
|
||||
* Used to transform API response keys to match TypeScript camelCase types.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Converts a snake_case string to camelCase.
|
||||
*/
|
||||
export function snakeToCamelCase(str: string): string {
|
||||
return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache for tracking visited objects during deep transformation.
|
||||
* Prevents infinite loops with circular references.
|
||||
*/
|
||||
type TransformCache = WeakMap<object, any>;
|
||||
|
||||
/**
|
||||
* Deeply transforms all keys in an object from snake_case to camelCase.
|
||||
* Handles nested objects, arrays, null, undefined, and primitive values.
|
||||
* Uses WeakMap caching to handle circular references safely.
|
||||
*/
|
||||
export function transformKeysToCamelCase<T>(value: unknown, cache?: TransformCache): T {
|
||||
// Handle null and undefined
|
||||
if (value === null || value === undefined) {
|
||||
return value as T;
|
||||
}
|
||||
|
||||
// Handle primitives (string, number, boolean, symbol, bigint)
|
||||
if (typeof value !== 'object') {
|
||||
return value as T;
|
||||
}
|
||||
|
||||
// Handle Date objects - no keys to transform
|
||||
if (value instanceof Date) {
|
||||
return value as T;
|
||||
}
|
||||
|
||||
// Handle Blob objects - no keys to transform
|
||||
if (value instanceof Blob) {
|
||||
return value as T;
|
||||
}
|
||||
|
||||
// Initialize cache if not provided
|
||||
const localCache = cache ?? new WeakMap();
|
||||
|
||||
// Check cache for circular references
|
||||
if (localCache.has(value as object)) {
|
||||
return localCache.get(value as object);
|
||||
}
|
||||
|
||||
// Handle arrays
|
||||
if (Array.isArray(value)) {
|
||||
const result: unknown[] = [];
|
||||
localCache.set(value, result);
|
||||
for (const item of value) {
|
||||
result.push(transformKeysToCamelCase(item, localCache));
|
||||
}
|
||||
return result as T;
|
||||
}
|
||||
|
||||
// Handle plain objects
|
||||
const result: Record<string, unknown> = {};
|
||||
localCache.set(value as object, result);
|
||||
|
||||
for (const key in value) {
|
||||
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
||||
const camelKey = snakeToCamelCase(key);
|
||||
const itemValue = (value as Record<string, unknown>)[key];
|
||||
result[camelKey] = transformKeysToCamelCase(itemValue, localCache);
|
||||
}
|
||||
}
|
||||
|
||||
return result as T;
|
||||
}
|
||||
@@ -56,3 +56,8 @@ export type OpResponseBodyPdf<O> = O extends {
|
||||
}
|
||||
? R
|
||||
: Blob;
|
||||
|
||||
/**
|
||||
* Case transformation utilities.
|
||||
*/
|
||||
export * from './case-transform';
|
||||
|
||||
Reference in New Issue
Block a user