From 028cf2aea8f76ba6710351b3b15041893ecc8857 Mon Sep 17 00:00:00 2001 From: ghost2023 Date: Wed, 20 May 2026 16:35:03 +0300 Subject: [PATCH] feat: implement better api service --- .../portal/src/services/api.ts | 191 ++++++++++++++++++ apps/edr-freight-web/portal/src/utils/api.ts | 86 ++++++++ 2 files changed, 277 insertions(+) create mode 100644 apps/edr-freight-web/portal/src/services/api.ts diff --git a/apps/edr-freight-web/portal/src/services/api.ts b/apps/edr-freight-web/portal/src/services/api.ts new file mode 100644 index 000000000..115ee1937 --- /dev/null +++ b/apps/edr-freight-web/portal/src/services/api.ts @@ -0,0 +1,191 @@ +import type { Freight, PaginatedResponse } from "@edr/types"; +import { client, endpoint, unwrap } from "@/utils/api"; +import type { + CreateFileUploadFieldDto, + CreateFileUploadSettingDto, + FileUploadField, + FileUploadSetting, + UpdateFileUploadFieldDto, + UpdateFileUploadSettingDto, +} from "@/types/fileUploadSettings"; + +// --------------------------------------------------------------------------- +// API definition +// --------------------------------------------------------------------------- + +export const api = { + bookings: { + list: endpoint>( + "bookings", + "list", + async () => { + const { data } = await client.get("/bookings"); + return unwrap(data); + }, + ), + + get: endpoint<{ id: string }, Freight.IBooking>( + "bookings", + "get", + async ({ id }) => { + const { data } = await client.get(`/bookings/${id}`); + return unwrap(data); + }, + ), + + create: endpoint< + { + reference: string; + customerId: string; + scheduledDate: string; + totalAmount: number; + trainId?: string; + }, + Freight.IBooking + >("bookings", "create", async (input) => { + const { data } = await client.post("/bookings", input); + return unwrap(data); + }), + + remove: endpoint<{ id: string }, void>( + "bookings", + "remove", + async ({ id }) => { + await client.delete(`/bookings/${id}`); + }, + ), + }, + + consignments: { + list: endpoint>( + "consignments", + "list", + async () => { + const { data } = await client.get("/consignments"); + return unwrap(data); + }, + ), + + get: endpoint<{ id: string }, Freight.IConsignment>( + "consignments", + "get", + async ({ id }) => { + const { data } = await client.get(`/consignments/${id}`); + return unwrap(data); + }, + ), + }, + + tracking: { + forConsignment: endpoint< + { consignmentId: string }, + Freight.ITrackingEvent[] + >("tracking", "forConsignment", async ({ consignmentId }) => { + const { data } = await client.get(`/tracking/${consignmentId}`); + return unwrap(data); + }), + }, + + fileUploadSettings: { + list: endpoint( + "file-upload-settings", + "list", + async () => { + const { data } = await client.get("/api/file-upload-settings"); + return unwrap(data); + }, + ), + + getById: endpoint<{ id: string }, FileUploadSetting>( + "file-upload-settings", + "getById", + async ({ id }) => { + const { data } = await client.get(`/api/file-upload-settings/${id}`); + return unwrap(data); + }, + ), + + getByCode: endpoint<{ code: string }, FileUploadSetting>( + "file-upload-settings", + "getByCode", + async ({ code }) => { + const { data } = await client.get( + `/api/file-upload-settings/by-code/${encodeURIComponent(code)}`, + ); + return unwrap(data); + }, + ), + + create: endpoint( + "file-upload-settings", + "create", + async (payload) => { + const { data } = await client.post( + "/api/file-upload-settings", + payload, + ); + return unwrap(data); + }, + ), + + update: endpoint< + { id: string; dto: UpdateFileUploadSettingDto }, + FileUploadSetting + >("file-upload-settings", "update", async ({ id, dto }) => { + const { data } = await client.patch( + `/api/file-upload-settings/${id}`, + dto, + ); + return unwrap(data); + }), + + remove: endpoint<{ id: string }, void>( + "file-upload-settings", + "remove", + async ({ id }) => { + await client.delete(`/api/file-upload-settings/${id}`); + }, + ), + + replaceFields: endpoint< + { id: string; fields: CreateFileUploadFieldDto[] }, + FileUploadField[] + >("file-upload-settings", "replaceFields", async ({ id, fields }) => { + const { data } = await client.put( + `/api/file-upload-settings/${id}/fields`, + fields, + ); + return unwrap(data); + }), + + addField: endpoint< + { settingId: string; dto: CreateFileUploadFieldDto }, + FileUploadField + >("file-upload-settings", "addField", async ({ settingId, dto }) => { + const { data } = await client.post( + `/api/file-upload-settings/${settingId}/fields`, + dto, + ); + return unwrap(data); + }), + + updateField: endpoint< + { fieldId: string; dto: UpdateFileUploadFieldDto }, + FileUploadField + >("file-upload-settings", "updateField", async ({ fieldId, dto }) => { + const { data } = await client.patch( + `/api/file-upload-settings/fields/${fieldId}`, + dto, + ); + return unwrap(data); + }), + + removeField: endpoint<{ fieldId: string }, void>( + "file-upload-settings", + "removeField", + async ({ fieldId }) => { + await client.delete(`/api/file-upload-settings/fields/${fieldId}`); + }, + ), + }, +}; diff --git a/apps/edr-freight-web/portal/src/utils/api.ts b/apps/edr-freight-web/portal/src/utils/api.ts index ebef6227e..f74dc51f4 100644 --- a/apps/edr-freight-web/portal/src/utils/api.ts +++ b/apps/edr-freight-web/portal/src/utils/api.ts @@ -1,3 +1,4 @@ +import { QueryObserverOptions, UseQueryOptions } from "@tanstack/react-query"; import axios from "axios"; export const client = axios.create({ @@ -24,3 +25,88 @@ client.interceptors.response.use( return Promise.reject(error); }, ); + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +export type QueryConfig = Omit< + UseQueryOptions, + "queryKey" | "queryFn" +>; + +export interface EndpointWithInput { + call(input: TInput): Promise; + queryKey(): readonly unknown[]; + queryKey(input: TInput): readonly unknown[]; + queryOptions( + config: { input: TInput } & QueryConfig, + ): UseQueryOptions; +} + +export interface EndpointWithoutInput { + call(): Promise; + queryKey(): readonly unknown[]; + queryOptions( + config?: QueryConfig, + ): UseQueryOptions; +} + +export type Endpoint = TInput extends void + ? EndpointWithoutInput + : EndpointWithInput; + +// --------------------------------------------------------------------------- +// Builder +// --------------------------------------------------------------------------- + +export function endpoint( + service: string, + action: string, + execute: (input: TInput) => Promise, +): Endpoint { + const call = (input: TInput) => execute(input); + + const queryKey = (input?: TInput): readonly unknown[] => { + if (input === undefined) return [service, action] as const; + return [service, action, input] as const; + }; + + const queryOptions = ( + config?: Record, + ): QueryObserverOptions< + TResponse, + Error, + TResponse, + TResponse, + readonly unknown[] + > => { + const input = config?.input as TInput | undefined; + const { input: _, ...rest } = config ?? {}; + const key: readonly unknown[] = + input !== undefined ? [service, action, input] : [service, action]; + return { + ...rest, + queryKey: key, + queryFn: () => execute(input as TInput), + } as ReturnType; + }; + + return { call, queryKey, queryOptions } as Endpoint; +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +/** Some API endpoints return `{ data: T }`, others return `T` directly. */ +export function unwrap(response: { data: T } | T): T { + if ( + response && + typeof response === "object" && + "data" in (response as object) + ) { + return (response as { data: T }).data; + } + return response as T; +}