From 8685d7ef187c94bd6ba71031de3c58b82a81138f Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Sat, 14 Mar 2026 23:24:31 +0200 Subject: [PATCH] feat(sdk-ts): settings fetch utils --- shared/sdk-ts/src/fetch-utils.ts | 46 ++++++++++ .../src/middleware/camel-case-middleware.ts | 28 ++++++ shared/sdk-ts/src/settings.ts | 86 +++++++++++++++++++ shared/sdk-ts/src/utils/case-transform.ts | 76 ++++++++++++++++ shared/sdk-ts/src/utils/index.ts | 5 ++ 5 files changed, 241 insertions(+) create mode 100644 shared/sdk-ts/src/middleware/camel-case-middleware.ts create mode 100644 shared/sdk-ts/src/utils/case-transform.ts diff --git a/shared/sdk-ts/src/fetch-utils.ts b/shared/sdk-ts/src/fetch-utils.ts index 8e584d98c..a9fdb2284 100644 --- a/shared/sdk-ts/src/fetch-utils.ts +++ b/shared/sdk-ts/src/fetch-utils.ts @@ -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>; 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(); 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( + fetcher: ApiFetcher, + method: string, + path: string, + body?: Record +): Promise { + // 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 = { + 'Accept': 'application/json', + ...(init.headers as Record || {}), + }; + + 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; +} diff --git a/shared/sdk-ts/src/middleware/camel-case-middleware.ts b/shared/sdk-ts/src/middleware/camel-case-middleware.ts new file mode 100644 index 000000000..a979bc2f6 --- /dev/null +++ b/shared/sdk-ts/src/middleware/camel-case-middleware.ts @@ -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 => { + 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), + }; + }; +} diff --git a/shared/sdk-ts/src/settings.ts b/shared/sdk-ts/src/settings.ts index fba8695ea..6882fa68d 100644 --- a/shared/sdk-ts/src/settings.ts +++ b/shared/sdk-ts/src/settings.ts @@ -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 { + return fetchSettings(fetcher, { group: 'sale_invoices' } as GetSettingsQuery); +} + +export async function fetchSettingsEstimates( + fetcher: ApiFetcher +): Promise { + return fetchSettings(fetcher, { group: 'sale_estimates' } as GetSettingsQuery); +} + +export async function fetchSettingsPaymentReceives( + fetcher: ApiFetcher +): Promise { + return fetchSettings(fetcher, { group: 'payment_receives' } as GetSettingsQuery); +} + +export async function fetchSettingsReceipts( + fetcher: ApiFetcher +): Promise { + return fetchSettings(fetcher, { group: 'sale_receipts' } as GetSettingsQuery); +} + +export async function fetchSettingsManualJournals( + fetcher: ApiFetcher +): Promise { + return fetchSettings(fetcher, { group: 'manual_journals' } as GetSettingsQuery); +} + +export async function fetchSettingsItems( + fetcher: ApiFetcher +): Promise { + return fetchSettings(fetcher, { group: 'items' } as GetSettingsQuery); +} + +export async function fetchSettingCashFlow( + fetcher: ApiFetcher +): Promise { + return fetchSettings(fetcher, { group: 'cashflow' } as GetSettingsQuery); +} + +export async function fetchSettingsCreditNotes( + fetcher: ApiFetcher +): Promise { + return fetchSettings(fetcher, { group: 'credit_note' } as GetSettingsQuery); +} + +export async function fetchSettingsVendorCredits( + fetcher: ApiFetcher +): Promise { + return fetchSettings(fetcher, { group: 'vendor_credit' } as GetSettingsQuery); +} + +export async function fetchSettingsWarehouseTransfers( + fetcher: ApiFetcher +): Promise { + 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 { + return rawRequest(fetcher, 'GET', '/api/settings/sms-notifications'); +} + +export async function fetchSettingSMSNotification( + fetcher: ApiFetcher, + key: string +): Promise { + return rawRequest(fetcher, 'GET', `/api/settings/sms-notification/${encodeURIComponent(key)}`); +} + +export async function editSettingSMSNotification( + fetcher: ApiFetcher, + key: string, + values: Record +): Promise { + return rawRequest(fetcher, 'POST', '/api/settings/sms-notification', { key, ...values }); +} diff --git a/shared/sdk-ts/src/utils/case-transform.ts b/shared/sdk-ts/src/utils/case-transform.ts new file mode 100644 index 000000000..cc9df355c --- /dev/null +++ b/shared/sdk-ts/src/utils/case-transform.ts @@ -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; + +/** + * 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(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 = {}; + 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)[key]; + result[camelKey] = transformKeysToCamelCase(itemValue, localCache); + } + } + + return result as T; +} diff --git a/shared/sdk-ts/src/utils/index.ts b/shared/sdk-ts/src/utils/index.ts index 52784044d..fed3b1b2b 100644 --- a/shared/sdk-ts/src/utils/index.ts +++ b/shared/sdk-ts/src/utils/index.ts @@ -56,3 +56,8 @@ export type OpResponseBodyPdf = O extends { } ? R : Blob; + +/** + * Case transformation utilities. + */ +export * from './case-transform';