feat(sdk-ts): settings fetch utils

This commit is contained in:
Ahmed Bouhuolia
2026-03-14 23:24:31 +02:00
parent 6515bd2a60
commit 8685d7ef18
5 changed files with 241 additions and 0 deletions

View File

@@ -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>;
}

View 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),
};
};
}

View File

@@ -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 });
}

View 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;
}

View File

@@ -56,3 +56,8 @@ export type OpResponseBodyPdf<O> = O extends {
}
? R
: Blob;
/**
* Case transformation utilities.
*/
export * from './case-transform';