feat: generate NSwag TypeScript client and authenticated API wrapper

This commit is contained in:
Martin 2026-03-20 01:10:23 +01:00
parent 40b256b870
commit 26e3efed81
3 changed files with 572 additions and 0 deletions

View File

@ -0,0 +1,75 @@
{
"runtime": "Default",
"defaultVariables": null,
"documentGenerator": {
"fromDocument": {
"url": "openapi.json",
"output": null,
"newLineBehavior": "Auto"
}
},
"codeGenerators": {
"openApiToTypeScriptClient": {
"className": "{controller}Client",
"moduleName": "",
"namespace": "",
"typeScriptVersion": 5.0,
"template": "Fetch",
"promiseType": "Promise",
"httpClass": "HttpClient",
"withCredentials": false,
"useSingletonProvider": false,
"injectionTokenType": "OpaqueToken",
"rxJsVersion": 6.0,
"dateTimeType": "String",
"nullValue": "Undefined",
"generateClientClasses": true,
"generateClientInterfaces": false,
"generateOptionalParameters": false,
"exportTypes": true,
"wrapDtoExceptions": false,
"exceptionClass": "SwaggerException",
"clientBaseClass": null,
"wrapResponses": false,
"wrapResponseMethods": [],
"generateResponseClasses": true,
"responseClass": "SwaggerResponse",
"protectedMethods": [],
"configurationClass": null,
"useTransformOptionsMethod": false,
"useTransformResultMethod": false,
"generateDtoTypes": true,
"operationGenerationMode": "MultipleClientsFromOperationId",
"includedOperationIds": [],
"excludedOperationIds": [],
"markOptionalProperties": true,
"generateCloneMethod": false,
"typeStyle": "Interface",
"enumStyle": "Enum",
"useLeafType": false,
"classTypes": [],
"extendedClasses": [],
"extensionCode": null,
"generateDefaultValues": true,
"excludedTypeNames": [],
"excludedParameterNames": [],
"handleReferences": false,
"generateTypeCheckFunctions": false,
"generateConstructorInterface": true,
"convertConstructorInterfaceData": false,
"importRequiredTypes": true,
"useGetBaseUrlMethod": false,
"baseUrlTokenName": "API_BASE_URL",
"queryNullValue": "",
"useAbortSignal": false,
"inlineNamedDictionaries": false,
"inlineNamedAny": false,
"includeHttpContext": false,
"templateDirectory": null,
"serviceHost": null,
"serviceSchemes": null,
"output": "src/api/apiClient.ts",
"newLineBehavior": "Auto"
}
}
}

View File

@ -0,0 +1,470 @@
//----------------------
// <auto-generated>
// Generated using the NSwag toolchain v14.6.3.0 (NJsonSchema v11.5.2.0 (Newtonsoft.Json v13.0.0.0)) (http://NSwag.org)
// </auto-generated>
//----------------------
/* eslint-disable */
// ReSharper disable InconsistentNaming
export class AuthClient {
private http: { fetch(url: RequestInfo, init?: RequestInit): Promise<Response> };
private baseUrl: string;
protected jsonParseReviver: ((key: string, value: any) => any) | undefined = undefined;
constructor(baseUrl?: string, http?: { fetch(url: RequestInfo, init?: RequestInit): Promise<Response> }) {
this.http = http ? http : window as any;
this.baseUrl = baseUrl ?? "http://localhost:5099";
}
login(request: LoginRequest): Promise<LoginResponse> {
let url_ = this.baseUrl + "/api/auth/login";
url_ = url_.replace(/[?&]$/, "");
const content_ = JSON.stringify(request);
let options_: RequestInit = {
body: content_,
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "application/json"
}
};
return this.http.fetch(url_, options_).then((_response: Response) => {
return this.processLogin(_response);
});
}
protected processLogin(response: Response): Promise<LoginResponse> {
const status = response.status;
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
if (status === 200) {
return response.text().then((_responseText) => {
let result200: any = null;
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as LoginResponse;
return result200;
});
} else if (status === 401) {
return response.text().then((_responseText) => {
let result401: any = null;
result401 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as ProblemDetails;
return throwException("A server side error occurred.", status, _responseText, _headers, result401);
});
} else if (status !== 200 && status !== 204) {
return response.text().then((_responseText) => {
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
});
}
return Promise.resolve<LoginResponse>(null as any);
}
}
export class DashboardClient {
private http: { fetch(url: RequestInfo, init?: RequestInit): Promise<Response> };
private baseUrl: string;
protected jsonParseReviver: ((key: string, value: any) => any) | undefined = undefined;
constructor(baseUrl?: string, http?: { fetch(url: RequestInfo, init?: RequestInit): Promise<Response> }) {
this.http = http ? http : window as any;
this.baseUrl = baseUrl ?? "http://localhost:5099";
}
summary(year: number | undefined, month: number | null | undefined): Promise<SummaryDto> {
let url_ = this.baseUrl + "/api/dashboard/summary?";
if (year === null)
throw new globalThis.Error("The parameter 'year' cannot be null.");
else if (year !== undefined)
url_ += "year=" + encodeURIComponent("" + year) + "&";
if (month !== undefined && month !== null)
url_ += "month=" + encodeURIComponent("" + month) + "&";
url_ = url_.replace(/[?&]$/, "");
let options_: RequestInit = {
method: "GET",
headers: {
"Accept": "application/json"
}
};
return this.http.fetch(url_, options_).then((_response: Response) => {
return this.processSummary(_response);
});
}
protected processSummary(response: Response): Promise<SummaryDto> {
const status = response.status;
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
if (status === 200) {
return response.text().then((_responseText) => {
let result200: any = null;
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as SummaryDto;
return result200;
});
} else if (status !== 200 && status !== 204) {
return response.text().then((_responseText) => {
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
});
}
return Promise.resolve<SummaryDto>(null as any);
}
spendingByCategory(year: number | undefined, month: number | null | undefined): Promise<CategorySpendingDto[]> {
let url_ = this.baseUrl + "/api/dashboard/spending-by-category?";
if (year === null)
throw new globalThis.Error("The parameter 'year' cannot be null.");
else if (year !== undefined)
url_ += "year=" + encodeURIComponent("" + year) + "&";
if (month !== undefined && month !== null)
url_ += "month=" + encodeURIComponent("" + month) + "&";
url_ = url_.replace(/[?&]$/, "");
let options_: RequestInit = {
method: "GET",
headers: {
"Accept": "application/json"
}
};
return this.http.fetch(url_, options_).then((_response: Response) => {
return this.processSpendingByCategory(_response);
});
}
protected processSpendingByCategory(response: Response): Promise<CategorySpendingDto[]> {
const status = response.status;
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
if (status === 200) {
return response.text().then((_responseText) => {
let result200: any = null;
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as CategorySpendingDto[];
return result200;
});
} else if (status !== 200 && status !== 204) {
return response.text().then((_responseText) => {
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
});
}
return Promise.resolve<CategorySpendingDto[]>(null as any);
}
monthlyBalances(year: number | undefined): Promise<MonthlyBalanceDto[]> {
let url_ = this.baseUrl + "/api/dashboard/monthly-balances?";
if (year === null)
throw new globalThis.Error("The parameter 'year' cannot be null.");
else if (year !== undefined)
url_ += "year=" + encodeURIComponent("" + year) + "&";
url_ = url_.replace(/[?&]$/, "");
let options_: RequestInit = {
method: "GET",
headers: {
"Accept": "application/json"
}
};
return this.http.fetch(url_, options_).then((_response: Response) => {
return this.processMonthlyBalances(_response);
});
}
protected processMonthlyBalances(response: Response): Promise<MonthlyBalanceDto[]> {
const status = response.status;
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
if (status === 200) {
return response.text().then((_responseText) => {
let result200: any = null;
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as MonthlyBalanceDto[];
return result200;
});
} else if (status !== 200 && status !== 204) {
return response.text().then((_responseText) => {
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
});
}
return Promise.resolve<MonthlyBalanceDto[]>(null as any);
}
cumulativeSpending(year: number | undefined, month: number | undefined): Promise<CumulativeSpendingDto[]> {
let url_ = this.baseUrl + "/api/dashboard/cumulative-spending?";
if (year === null)
throw new globalThis.Error("The parameter 'year' cannot be null.");
else if (year !== undefined)
url_ += "year=" + encodeURIComponent("" + year) + "&";
if (month === null)
throw new globalThis.Error("The parameter 'month' cannot be null.");
else if (month !== undefined)
url_ += "month=" + encodeURIComponent("" + month) + "&";
url_ = url_.replace(/[?&]$/, "");
let options_: RequestInit = {
method: "GET",
headers: {
"Accept": "application/json"
}
};
return this.http.fetch(url_, options_).then((_response: Response) => {
return this.processCumulativeSpending(_response);
});
}
protected processCumulativeSpending(response: Response): Promise<CumulativeSpendingDto[]> {
const status = response.status;
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
if (status === 200) {
return response.text().then((_responseText) => {
let result200: any = null;
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as CumulativeSpendingDto[];
return result200;
});
} else if (status !== 200 && status !== 204) {
return response.text().then((_responseText) => {
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
});
}
return Promise.resolve<CumulativeSpendingDto[]>(null as any);
}
}
export class TransactionsClient {
private http: { fetch(url: RequestInfo, init?: RequestInit): Promise<Response> };
private baseUrl: string;
protected jsonParseReviver: ((key: string, value: any) => any) | undefined = undefined;
constructor(baseUrl?: string, http?: { fetch(url: RequestInfo, init?: RequestInit): Promise<Response> }) {
this.http = http ? http : window as any;
this.baseUrl = baseUrl ?? "http://localhost:5099";
}
import(file: FileParameter | null | undefined): Promise<ImportResult> {
let url_ = this.baseUrl + "/api/transactions/import";
url_ = url_.replace(/[?&]$/, "");
const content_ = new FormData();
if (file !== null && file !== undefined)
content_.append("file", file.data, file.fileName ? file.fileName : "file");
let options_: RequestInit = {
body: content_,
method: "POST",
headers: {
"Accept": "application/json"
}
};
return this.http.fetch(url_, options_).then((_response: Response) => {
return this.processImport(_response);
});
}
protected processImport(response: Response): Promise<ImportResult> {
const status = response.status;
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
if (status === 200) {
return response.text().then((_responseText) => {
let result200: any = null;
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as ImportResult;
return result200;
});
} else if (status === 400) {
return response.text().then((_responseText) => {
let result400: any = null;
result400 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as ErrorResult;
return throwException("A server side error occurred.", status, _responseText, _headers, result400);
});
} else if (status !== 200 && status !== 204) {
return response.text().then((_responseText) => {
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
});
}
return Promise.resolve<ImportResult>(null as any);
}
list(year: number | undefined, month: number | null | undefined, category: string | null | undefined, search: string | null | undefined, page: number | undefined, pageSize: number | undefined): Promise<TransactionListResponse> {
let url_ = this.baseUrl + "/api/transactions?";
if (year === null)
throw new globalThis.Error("The parameter 'year' cannot be null.");
else if (year !== undefined)
url_ += "year=" + encodeURIComponent("" + year) + "&";
if (month !== undefined && month !== null)
url_ += "month=" + encodeURIComponent("" + month) + "&";
if (category !== undefined && category !== null)
url_ += "category=" + encodeURIComponent("" + category) + "&";
if (search !== undefined && search !== null)
url_ += "search=" + encodeURIComponent("" + search) + "&";
if (page === null)
throw new globalThis.Error("The parameter 'page' cannot be null.");
else if (page !== undefined)
url_ += "page=" + encodeURIComponent("" + page) + "&";
if (pageSize === null)
throw new globalThis.Error("The parameter 'pageSize' cannot be null.");
else if (pageSize !== undefined)
url_ += "pageSize=" + encodeURIComponent("" + pageSize) + "&";
url_ = url_.replace(/[?&]$/, "");
let options_: RequestInit = {
method: "GET",
headers: {
"Accept": "application/json"
}
};
return this.http.fetch(url_, options_).then((_response: Response) => {
return this.processList(_response);
});
}
protected processList(response: Response): Promise<TransactionListResponse> {
const status = response.status;
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
if (status === 200) {
return response.text().then((_responseText) => {
let result200: any = null;
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as TransactionListResponse;
return result200;
});
} else if (status !== 200 && status !== 204) {
return response.text().then((_responseText) => {
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
});
}
return Promise.resolve<TransactionListResponse>(null as any);
}
categories(): Promise<string[]> {
let url_ = this.baseUrl + "/api/transactions/categories";
url_ = url_.replace(/[?&]$/, "");
let options_: RequestInit = {
method: "GET",
headers: {
"Accept": "application/json"
}
};
return this.http.fetch(url_, options_).then((_response: Response) => {
return this.processCategories(_response);
});
}
protected processCategories(response: Response): Promise<string[]> {
const status = response.status;
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
if (status === 200) {
return response.text().then((_responseText) => {
let result200: any = null;
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as string[];
return result200;
});
} else if (status !== 200 && status !== 204) {
return response.text().then((_responseText) => {
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
});
}
return Promise.resolve<string[]>(null as any);
}
}
export interface LoginResponse {
token?: string;
expiresAt?: string;
}
export interface ProblemDetails {
type?: string | undefined;
title?: string | undefined;
status?: number | undefined;
detail?: string | undefined;
instance?: string | undefined;
[key: string]: any;
}
export interface LoginRequest {
username?: string;
password?: string;
}
export interface SummaryDto {
totalSpent?: number;
totalIncome?: number;
}
export interface CategorySpendingDto {
category?: string;
total?: number;
}
export interface MonthlyBalanceDto {
month?: number;
closingBalance?: number;
}
export interface CumulativeSpendingDto {
day?: number;
cumulativeSpent?: number;
}
export interface ImportResult {
recordsImported?: number;
recordsSkipped?: number;
}
export interface ErrorResult {
error?: string;
}
export interface TransactionListResponse {
items?: TransactionDto[];
totalCount?: number;
page?: number;
pageSize?: number;
}
export interface TransactionDto {
id?: number;
bookingDate?: string;
counterPartyName?: string | undefined;
category?: string | undefined;
amount?: number;
balance?: number;
message?: string | undefined;
}
export interface FileParameter {
data: any;
fileName: string;
}
export class SwaggerException extends Error {
override message: string;
status: number;
response: string;
headers: { [key: string]: any; };
result: any;
constructor(message: string, status: number, response: string, headers: { [key: string]: any; }, result: any) {
super();
this.message = message;
this.status = status;
this.response = response;
this.headers = headers;
this.result = result;
}
protected isSwaggerException = true;
static isSwaggerException(obj: any): obj is SwaggerException {
return obj.isSwaggerException === true;
}
}
function throwException(message: string, status: number, response: string, headers: { [key: string]: any; }, result?: any): any {
if (result !== null && result !== undefined)
throw result;
else
throw new SwaggerException(message, status, response, headers, null);
}

View File

@ -0,0 +1,27 @@
import { AuthClient, TransactionsClient, DashboardClient } from './apiClient'
// Custom fetch that injects the JWT and handles 401
class AuthFetch {
fetch(url: RequestInfo, init?: RequestInit): Promise<Response> {
const token = localStorage.getItem('token')
const headers = new Headers(init?.headers)
if (token) headers.set('Authorization', `Bearer ${token}`)
return fetch(url, { ...init, headers }).then((res) => {
if (res.status === 401) {
localStorage.removeItem('token')
localStorage.removeItem('expiresAt')
// Navigate to login — import router lazily to avoid circular dep
import('../router').then(({ default: router }) => router.push('/login'))
}
return res
})
}
}
const httpClient = new AuthFetch()
// Base URL is empty: NSwag generates full paths including /api/ prefix
export const authApi = new AuthClient('', httpClient)
export const transactionsApi = new TransactionsClient('', httpClient)
export const dashboardApi = new DashboardClient('', httpClient)