diff --git a/modules/frontend/src/App.tsx b/modules/frontend/src/App.tsx index 84c9086..67336c1 100644 --- a/modules/frontend/src/App.tsx +++ b/modules/frontend/src/App.tsx @@ -6,9 +6,10 @@ import TitlePage from "./pages/TitlePage/TitlePage"; import { LoginPage } from "./pages/LoginPage/LoginPage"; import { Header } from "./components/Header/Header"; -// import { OpenAPI } from "./api"; +import { OpenAPI } from "./api"; -// OpenAPI.WITH_CREDENTIALS = true +OpenAPI.WITH_CREDENTIALS = true +OpenAPI.CREDENTIALS = 'include' const App: React.FC = () => { const username = localStorage.getItem("username") || undefined; diff --git a/modules/frontend/src/api/client.gen.ts b/modules/frontend/src/api/client.gen.ts deleted file mode 100644 index 2de06ac..0000000 --- a/modules/frontend/src/api/client.gen.ts +++ /dev/null @@ -1,16 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import { type ClientOptions, type Config, createClient, createConfig } from './client'; -import type { ClientOptions as ClientOptions2 } from './types.gen'; - -/** - * The `createClientConfig()` function will be called on client initialization - * and the returned object will become the client's initial configuration. - * - * You may want to initialize your client this way instead of calling - * `setConfig()`. This is useful for example if you're using Next.js - * to ensure your client always has the correct values. - */ -export type CreateClientConfig = (override?: Config) => Config & T>; - -export const client = createClient(createConfig({ baseUrl: 'http://10.1.0.65:8081/api/v1' })); diff --git a/modules/frontend/src/api/client/client.gen.ts b/modules/frontend/src/api/client/client.gen.ts deleted file mode 100644 index c2a5190..0000000 --- a/modules/frontend/src/api/client/client.gen.ts +++ /dev/null @@ -1,301 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import { createSseClient } from '../core/serverSentEvents.gen'; -import type { HttpMethod } from '../core/types.gen'; -import { getValidRequestBody } from '../core/utils.gen'; -import type { - Client, - Config, - RequestOptions, - ResolvedRequestOptions, -} from './types.gen'; -import { - buildUrl, - createConfig, - createInterceptors, - getParseAs, - mergeConfigs, - mergeHeaders, - setAuthParams, -} from './utils.gen'; - -type ReqInit = Omit & { - body?: any; - headers: ReturnType; -}; - -export const createClient = (config: Config = {}): Client => { - let _config = mergeConfigs(createConfig(), config); - - const getConfig = (): Config => ({ ..._config }); - - const setConfig = (config: Config): Config => { - _config = mergeConfigs(_config, config); - return getConfig(); - }; - - const interceptors = createInterceptors< - Request, - Response, - unknown, - ResolvedRequestOptions - >(); - - const beforeRequest = async (options: RequestOptions) => { - const opts = { - ..._config, - ...options, - fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, - headers: mergeHeaders(_config.headers, options.headers), - serializedBody: undefined, - }; - - if (opts.security) { - await setAuthParams({ - ...opts, - security: opts.security, - }); - } - - if (opts.requestValidator) { - await opts.requestValidator(opts); - } - - if (opts.body !== undefined && opts.bodySerializer) { - opts.serializedBody = opts.bodySerializer(opts.body); - } - - // remove Content-Type header if body is empty to avoid sending invalid requests - if (opts.body === undefined || opts.serializedBody === '') { - opts.headers.delete('Content-Type'); - } - - const url = buildUrl(opts); - - return { opts, url }; - }; - - const request: Client['request'] = async (options) => { - // @ts-expect-error - const { opts, url } = await beforeRequest(options); - const requestInit: ReqInit = { - redirect: 'follow', - ...opts, - body: getValidRequestBody(opts), - }; - - let request = new Request(url, requestInit); - - for (const fn of interceptors.request.fns) { - if (fn) { - request = await fn(request, opts); - } - } - - // fetch must be assigned here, otherwise it would throw the error: - // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation - const _fetch = opts.fetch!; - let response: Response; - - try { - response = await _fetch(request); - } catch (error) { - // Handle fetch exceptions (AbortError, network errors, etc.) - let finalError = error; - - for (const fn of interceptors.error.fns) { - if (fn) { - finalError = (await fn( - error, - undefined as any, - request, - opts, - )) as unknown; - } - } - - finalError = finalError || ({} as unknown); - - if (opts.throwOnError) { - throw finalError; - } - - // Return error response - return opts.responseStyle === 'data' - ? undefined - : { - error: finalError, - request, - response: undefined as any, - }; - } - - for (const fn of interceptors.response.fns) { - if (fn) { - response = await fn(response, request, opts); - } - } - - const result = { - request, - response, - }; - - if (response.ok) { - const parseAs = - (opts.parseAs === 'auto' - ? getParseAs(response.headers.get('Content-Type')) - : opts.parseAs) ?? 'json'; - - if ( - response.status === 204 || - response.headers.get('Content-Length') === '0' - ) { - let emptyData: any; - switch (parseAs) { - case 'arrayBuffer': - case 'blob': - case 'text': - emptyData = await response[parseAs](); - break; - case 'formData': - emptyData = new FormData(); - break; - case 'stream': - emptyData = response.body; - break; - case 'json': - default: - emptyData = {}; - break; - } - return opts.responseStyle === 'data' - ? emptyData - : { - data: emptyData, - ...result, - }; - } - - let data: any; - switch (parseAs) { - case 'arrayBuffer': - case 'blob': - case 'formData': - case 'json': - case 'text': - data = await response[parseAs](); - break; - case 'stream': - return opts.responseStyle === 'data' - ? response.body - : { - data: response.body, - ...result, - }; - } - - if (parseAs === 'json') { - if (opts.responseValidator) { - await opts.responseValidator(data); - } - - if (opts.responseTransformer) { - data = await opts.responseTransformer(data); - } - } - - return opts.responseStyle === 'data' - ? data - : { - data, - ...result, - }; - } - - const textError = await response.text(); - let jsonError: unknown; - - try { - jsonError = JSON.parse(textError); - } catch { - // noop - } - - const error = jsonError ?? textError; - let finalError = error; - - for (const fn of interceptors.error.fns) { - if (fn) { - finalError = (await fn(error, response, request, opts)) as string; - } - } - - finalError = finalError || ({} as string); - - if (opts.throwOnError) { - throw finalError; - } - - // TODO: we probably want to return error and improve types - return opts.responseStyle === 'data' - ? undefined - : { - error: finalError, - ...result, - }; - }; - - const makeMethodFn = - (method: Uppercase) => (options: RequestOptions) => - request({ ...options, method }); - - const makeSseFn = - (method: Uppercase) => async (options: RequestOptions) => { - const { opts, url } = await beforeRequest(options); - return createSseClient({ - ...opts, - body: opts.body as BodyInit | null | undefined, - headers: opts.headers as unknown as Record, - method, - onRequest: async (url, init) => { - let request = new Request(url, init); - for (const fn of interceptors.request.fns) { - if (fn) { - request = await fn(request, opts); - } - } - return request; - }, - url, - }); - }; - - return { - buildUrl, - connect: makeMethodFn('CONNECT'), - delete: makeMethodFn('DELETE'), - get: makeMethodFn('GET'), - getConfig, - head: makeMethodFn('HEAD'), - interceptors, - options: makeMethodFn('OPTIONS'), - patch: makeMethodFn('PATCH'), - post: makeMethodFn('POST'), - put: makeMethodFn('PUT'), - request, - setConfig, - sse: { - connect: makeSseFn('CONNECT'), - delete: makeSseFn('DELETE'), - get: makeSseFn('GET'), - head: makeSseFn('HEAD'), - options: makeSseFn('OPTIONS'), - patch: makeSseFn('PATCH'), - post: makeSseFn('POST'), - put: makeSseFn('PUT'), - trace: makeSseFn('TRACE'), - }, - trace: makeMethodFn('TRACE'), - } as Client; -}; diff --git a/modules/frontend/src/api/client/index.ts b/modules/frontend/src/api/client/index.ts deleted file mode 100644 index b295ede..0000000 --- a/modules/frontend/src/api/client/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -export type { Auth } from '../core/auth.gen'; -export type { QuerySerializerOptions } from '../core/bodySerializer.gen'; -export { - formDataBodySerializer, - jsonBodySerializer, - urlSearchParamsBodySerializer, -} from '../core/bodySerializer.gen'; -export { buildClientParams } from '../core/params.gen'; -export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen'; -export { createClient } from './client.gen'; -export type { - Client, - ClientOptions, - Config, - CreateClientConfig, - Options, - RequestOptions, - RequestResult, - ResolvedRequestOptions, - ResponseStyle, - TDataShape, -} from './types.gen'; -export { createConfig, mergeHeaders } from './utils.gen'; diff --git a/modules/frontend/src/api/client/types.gen.ts b/modules/frontend/src/api/client/types.gen.ts deleted file mode 100644 index b4a499c..0000000 --- a/modules/frontend/src/api/client/types.gen.ts +++ /dev/null @@ -1,241 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { Auth } from '../core/auth.gen'; -import type { - ServerSentEventsOptions, - ServerSentEventsResult, -} from '../core/serverSentEvents.gen'; -import type { - Client as CoreClient, - Config as CoreConfig, -} from '../core/types.gen'; -import type { Middleware } from './utils.gen'; - -export type ResponseStyle = 'data' | 'fields'; - -export interface Config - extends Omit, - CoreConfig { - /** - * Base URL for all requests made by this client. - */ - baseUrl?: T['baseUrl']; - /** - * Fetch API implementation. You can use this option to provide a custom - * fetch instance. - * - * @default globalThis.fetch - */ - fetch?: typeof fetch; - /** - * Please don't use the Fetch client for Next.js applications. The `next` - * options won't have any effect. - * - * Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead. - */ - next?: never; - /** - * Return the response data parsed in a specified format. By default, `auto` - * will infer the appropriate method from the `Content-Type` response header. - * You can override this behavior with any of the {@link Body} methods. - * Select `stream` if you don't want to parse response data at all. - * - * @default 'auto' - */ - parseAs?: - | 'arrayBuffer' - | 'auto' - | 'blob' - | 'formData' - | 'json' - | 'stream' - | 'text'; - /** - * Should we return only data or multiple fields (data, error, response, etc.)? - * - * @default 'fields' - */ - responseStyle?: ResponseStyle; - /** - * Throw an error instead of returning it in the response? - * - * @default false - */ - throwOnError?: T['throwOnError']; -} - -export interface RequestOptions< - TData = unknown, - TResponseStyle extends ResponseStyle = 'fields', - ThrowOnError extends boolean = boolean, - Url extends string = string, -> extends Config<{ - responseStyle: TResponseStyle; - throwOnError: ThrowOnError; - }>, - Pick< - ServerSentEventsOptions, - | 'onSseError' - | 'onSseEvent' - | 'sseDefaultRetryDelay' - | 'sseMaxRetryAttempts' - | 'sseMaxRetryDelay' - > { - /** - * Any body that you want to add to your request. - * - * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} - */ - body?: unknown; - path?: Record; - query?: Record; - /** - * Security mechanism(s) to use for the request. - */ - security?: ReadonlyArray; - url: Url; -} - -export interface ResolvedRequestOptions< - TResponseStyle extends ResponseStyle = 'fields', - ThrowOnError extends boolean = boolean, - Url extends string = string, -> extends RequestOptions { - serializedBody?: string; -} - -export type RequestResult< - TData = unknown, - TError = unknown, - ThrowOnError extends boolean = boolean, - TResponseStyle extends ResponseStyle = 'fields', -> = ThrowOnError extends true - ? Promise< - TResponseStyle extends 'data' - ? TData extends Record - ? TData[keyof TData] - : TData - : { - data: TData extends Record - ? TData[keyof TData] - : TData; - request: Request; - response: Response; - } - > - : Promise< - TResponseStyle extends 'data' - ? - | (TData extends Record - ? TData[keyof TData] - : TData) - | undefined - : ( - | { - data: TData extends Record - ? TData[keyof TData] - : TData; - error: undefined; - } - | { - data: undefined; - error: TError extends Record - ? TError[keyof TError] - : TError; - } - ) & { - request: Request; - response: Response; - } - >; - -export interface ClientOptions { - baseUrl?: string; - responseStyle?: ResponseStyle; - throwOnError?: boolean; -} - -type MethodFn = < - TData = unknown, - TError = unknown, - ThrowOnError extends boolean = false, - TResponseStyle extends ResponseStyle = 'fields', ->( - options: Omit, 'method'>, -) => RequestResult; - -type SseFn = < - TData = unknown, - TError = unknown, - ThrowOnError extends boolean = false, - TResponseStyle extends ResponseStyle = 'fields', ->( - options: Omit, 'method'>, -) => Promise>; - -type RequestFn = < - TData = unknown, - TError = unknown, - ThrowOnError extends boolean = false, - TResponseStyle extends ResponseStyle = 'fields', ->( - options: Omit, 'method'> & - Pick< - Required>, - 'method' - >, -) => RequestResult; - -type BuildUrlFn = < - TData extends { - body?: unknown; - path?: Record; - query?: Record; - url: string; - }, ->( - options: TData & Options, -) => string; - -export type Client = CoreClient< - RequestFn, - Config, - MethodFn, - BuildUrlFn, - SseFn -> & { - interceptors: Middleware; -}; - -/** - * The `createClientConfig()` function will be called on client initialization - * and the returned object will become the client's initial configuration. - * - * You may want to initialize your client this way instead of calling - * `setConfig()`. This is useful for example if you're using Next.js - * to ensure your client always has the correct values. - */ -export type CreateClientConfig = ( - override?: Config, -) => Config & T>; - -export interface TDataShape { - body?: unknown; - headers?: unknown; - path?: unknown; - query?: unknown; - url: string; -} - -type OmitKeys = Pick>; - -export type Options< - TData extends TDataShape = TDataShape, - ThrowOnError extends boolean = boolean, - TResponse = unknown, - TResponseStyle extends ResponseStyle = 'fields', -> = OmitKeys< - RequestOptions, - 'body' | 'path' | 'query' | 'url' -> & - ([TData] extends [never] ? unknown : Omit); diff --git a/modules/frontend/src/api/client/utils.gen.ts b/modules/frontend/src/api/client/utils.gen.ts deleted file mode 100644 index 4c48a9e..0000000 --- a/modules/frontend/src/api/client/utils.gen.ts +++ /dev/null @@ -1,332 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import { getAuthToken } from '../core/auth.gen'; -import type { QuerySerializerOptions } from '../core/bodySerializer.gen'; -import { jsonBodySerializer } from '../core/bodySerializer.gen'; -import { - serializeArrayParam, - serializeObjectParam, - serializePrimitiveParam, -} from '../core/pathSerializer.gen'; -import { getUrl } from '../core/utils.gen'; -import type { Client, ClientOptions, Config, RequestOptions } from './types.gen'; - -export const createQuerySerializer = ({ - parameters = {}, - ...args -}: QuerySerializerOptions = {}) => { - const querySerializer = (queryParams: T) => { - const search: string[] = []; - if (queryParams && typeof queryParams === 'object') { - for (const name in queryParams) { - const value = queryParams[name]; - - if (value === undefined || value === null) { - continue; - } - - const options = parameters[name] || args; - - if (Array.isArray(value)) { - const serializedArray = serializeArrayParam({ - allowReserved: options.allowReserved, - explode: true, - name, - style: 'form', - value, - ...options.array, - }); - if (serializedArray) search.push(serializedArray); - } else if (typeof value === 'object') { - const serializedObject = serializeObjectParam({ - allowReserved: options.allowReserved, - explode: true, - name, - style: 'deepObject', - value: value as Record, - ...options.object, - }); - if (serializedObject) search.push(serializedObject); - } else { - const serializedPrimitive = serializePrimitiveParam({ - allowReserved: options.allowReserved, - name, - value: value as string, - }); - if (serializedPrimitive) search.push(serializedPrimitive); - } - } - } - return search.join('&'); - }; - return querySerializer; -}; - -/** - * Infers parseAs value from provided Content-Type header. - */ -export const getParseAs = ( - contentType: string | null, -): Exclude => { - if (!contentType) { - // If no Content-Type header is provided, the best we can do is return the raw response body, - // which is effectively the same as the 'stream' option. - return 'stream'; - } - - const cleanContent = contentType.split(';')[0]?.trim(); - - if (!cleanContent) { - return; - } - - if ( - cleanContent.startsWith('application/json') || - cleanContent.endsWith('+json') - ) { - return 'json'; - } - - if (cleanContent === 'multipart/form-data') { - return 'formData'; - } - - if ( - ['application/', 'audio/', 'image/', 'video/'].some((type) => - cleanContent.startsWith(type), - ) - ) { - return 'blob'; - } - - if (cleanContent.startsWith('text/')) { - return 'text'; - } - - return; -}; - -const checkForExistence = ( - options: Pick & { - headers: Headers; - }, - name?: string, -): boolean => { - if (!name) { - return false; - } - if ( - options.headers.has(name) || - options.query?.[name] || - options.headers.get('Cookie')?.includes(`${name}=`) - ) { - return true; - } - return false; -}; - -export const setAuthParams = async ({ - security, - ...options -}: Pick, 'security'> & - Pick & { - headers: Headers; - }) => { - for (const auth of security) { - if (checkForExistence(options, auth.name)) { - continue; - } - - const token = await getAuthToken(auth, options.auth); - - if (!token) { - continue; - } - - const name = auth.name ?? 'Authorization'; - - switch (auth.in) { - case 'query': - if (!options.query) { - options.query = {}; - } - options.query[name] = token; - break; - case 'cookie': - options.headers.append('Cookie', `${name}=${token}`); - break; - case 'header': - default: - options.headers.set(name, token); - break; - } - } -}; - -export const buildUrl: Client['buildUrl'] = (options) => - getUrl({ - baseUrl: options.baseUrl as string, - path: options.path, - query: options.query, - querySerializer: - typeof options.querySerializer === 'function' - ? options.querySerializer - : createQuerySerializer(options.querySerializer), - url: options.url, - }); - -export const mergeConfigs = (a: Config, b: Config): Config => { - const config = { ...a, ...b }; - if (config.baseUrl?.endsWith('/')) { - config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1); - } - config.headers = mergeHeaders(a.headers, b.headers); - return config; -}; - -const headersEntries = (headers: Headers): Array<[string, string]> => { - const entries: Array<[string, string]> = []; - headers.forEach((value, key) => { - entries.push([key, value]); - }); - return entries; -}; - -export const mergeHeaders = ( - ...headers: Array['headers'] | undefined> -): Headers => { - const mergedHeaders = new Headers(); - for (const header of headers) { - if (!header) { - continue; - } - - const iterator = - header instanceof Headers - ? headersEntries(header) - : Object.entries(header); - - for (const [key, value] of iterator) { - if (value === null) { - mergedHeaders.delete(key); - } else if (Array.isArray(value)) { - for (const v of value) { - mergedHeaders.append(key, v as string); - } - } else if (value !== undefined) { - // assume object headers are meant to be JSON stringified, i.e. their - // content value in OpenAPI specification is 'application/json' - mergedHeaders.set( - key, - typeof value === 'object' ? JSON.stringify(value) : (value as string), - ); - } - } - } - return mergedHeaders; -}; - -type ErrInterceptor = ( - error: Err, - response: Res, - request: Req, - options: Options, -) => Err | Promise; - -type ReqInterceptor = ( - request: Req, - options: Options, -) => Req | Promise; - -type ResInterceptor = ( - response: Res, - request: Req, - options: Options, -) => Res | Promise; - -class Interceptors { - fns: Array = []; - - clear(): void { - this.fns = []; - } - - eject(id: number | Interceptor): void { - const index = this.getInterceptorIndex(id); - if (this.fns[index]) { - this.fns[index] = null; - } - } - - exists(id: number | Interceptor): boolean { - const index = this.getInterceptorIndex(id); - return Boolean(this.fns[index]); - } - - getInterceptorIndex(id: number | Interceptor): number { - if (typeof id === 'number') { - return this.fns[id] ? id : -1; - } - return this.fns.indexOf(id); - } - - update( - id: number | Interceptor, - fn: Interceptor, - ): number | Interceptor | false { - const index = this.getInterceptorIndex(id); - if (this.fns[index]) { - this.fns[index] = fn; - return id; - } - return false; - } - - use(fn: Interceptor): number { - this.fns.push(fn); - return this.fns.length - 1; - } -} - -export interface Middleware { - error: Interceptors>; - request: Interceptors>; - response: Interceptors>; -} - -export const createInterceptors = (): Middleware< - Req, - Res, - Err, - Options -> => ({ - error: new Interceptors>(), - request: new Interceptors>(), - response: new Interceptors>(), -}); - -const defaultQuerySerializer = createQuerySerializer({ - allowReserved: false, - array: { - explode: true, - style: 'form', - }, - object: { - explode: true, - style: 'deepObject', - }, -}); - -const defaultHeaders = { - 'Content-Type': 'application/json', -}; - -export const createConfig = ( - override: Config & T> = {}, -): Config & T> => ({ - ...jsonBodySerializer, - headers: defaultHeaders, - parseAs: 'auto', - querySerializer: defaultQuerySerializer, - ...override, -}); diff --git a/modules/frontend/src/api/core/ApiError.ts b/modules/frontend/src/api/core/ApiError.ts new file mode 100644 index 0000000..ec7b16a --- /dev/null +++ b/modules/frontend/src/api/core/ApiError.ts @@ -0,0 +1,25 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { ApiRequestOptions } from './ApiRequestOptions'; +import type { ApiResult } from './ApiResult'; + +export class ApiError extends Error { + public readonly url: string; + public readonly status: number; + public readonly statusText: string; + public readonly body: any; + public readonly request: ApiRequestOptions; + + constructor(request: ApiRequestOptions, response: ApiResult, message: string) { + super(message); + + this.name = 'ApiError'; + this.url = response.url; + this.status = response.status; + this.statusText = response.statusText; + this.body = response.body; + this.request = request; + } +} diff --git a/modules/frontend/src/api/core/ApiRequestOptions.ts b/modules/frontend/src/api/core/ApiRequestOptions.ts new file mode 100644 index 0000000..93143c3 --- /dev/null +++ b/modules/frontend/src/api/core/ApiRequestOptions.ts @@ -0,0 +1,17 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type ApiRequestOptions = { + readonly method: 'GET' | 'PUT' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD' | 'PATCH'; + readonly url: string; + readonly path?: Record; + readonly cookies?: Record; + readonly headers?: Record; + readonly query?: Record; + readonly formData?: Record; + readonly body?: any; + readonly mediaType?: string; + readonly responseHeader?: string; + readonly errors?: Record; +}; diff --git a/modules/frontend/src/api/core/ApiResult.ts b/modules/frontend/src/api/core/ApiResult.ts new file mode 100644 index 0000000..ee1126e --- /dev/null +++ b/modules/frontend/src/api/core/ApiResult.ts @@ -0,0 +1,11 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type ApiResult = { + readonly url: string; + readonly ok: boolean; + readonly status: number; + readonly statusText: string; + readonly body: any; +}; diff --git a/modules/frontend/src/api/core/CancelablePromise.ts b/modules/frontend/src/api/core/CancelablePromise.ts new file mode 100644 index 0000000..d70de92 --- /dev/null +++ b/modules/frontend/src/api/core/CancelablePromise.ts @@ -0,0 +1,131 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export class CancelError extends Error { + + constructor(message: string) { + super(message); + this.name = 'CancelError'; + } + + public get isCancelled(): boolean { + return true; + } +} + +export interface OnCancel { + readonly isResolved: boolean; + readonly isRejected: boolean; + readonly isCancelled: boolean; + + (cancelHandler: () => void): void; +} + +export class CancelablePromise implements Promise { + #isResolved: boolean; + #isRejected: boolean; + #isCancelled: boolean; + readonly #cancelHandlers: (() => void)[]; + readonly #promise: Promise; + #resolve?: (value: T | PromiseLike) => void; + #reject?: (reason?: any) => void; + + constructor( + executor: ( + resolve: (value: T | PromiseLike) => void, + reject: (reason?: any) => void, + onCancel: OnCancel + ) => void + ) { + this.#isResolved = false; + this.#isRejected = false; + this.#isCancelled = false; + this.#cancelHandlers = []; + this.#promise = new Promise((resolve, reject) => { + this.#resolve = resolve; + this.#reject = reject; + + const onResolve = (value: T | PromiseLike): void => { + if (this.#isResolved || this.#isRejected || this.#isCancelled) { + return; + } + this.#isResolved = true; + if (this.#resolve) this.#resolve(value); + }; + + const onReject = (reason?: any): void => { + if (this.#isResolved || this.#isRejected || this.#isCancelled) { + return; + } + this.#isRejected = true; + if (this.#reject) this.#reject(reason); + }; + + const onCancel = (cancelHandler: () => void): void => { + if (this.#isResolved || this.#isRejected || this.#isCancelled) { + return; + } + this.#cancelHandlers.push(cancelHandler); + }; + + Object.defineProperty(onCancel, 'isResolved', { + get: (): boolean => this.#isResolved, + }); + + Object.defineProperty(onCancel, 'isRejected', { + get: (): boolean => this.#isRejected, + }); + + Object.defineProperty(onCancel, 'isCancelled', { + get: (): boolean => this.#isCancelled, + }); + + return executor(onResolve, onReject, onCancel as OnCancel); + }); + } + + get [Symbol.toStringTag]() { + return "Cancellable Promise"; + } + + public then( + onFulfilled?: ((value: T) => TResult1 | PromiseLike) | null, + onRejected?: ((reason: any) => TResult2 | PromiseLike) | null + ): Promise { + return this.#promise.then(onFulfilled, onRejected); + } + + public catch( + onRejected?: ((reason: any) => TResult | PromiseLike) | null + ): Promise { + return this.#promise.catch(onRejected); + } + + public finally(onFinally?: (() => void) | null): Promise { + return this.#promise.finally(onFinally); + } + + public cancel(): void { + if (this.#isResolved || this.#isRejected || this.#isCancelled) { + return; + } + this.#isCancelled = true; + if (this.#cancelHandlers.length) { + try { + for (const cancelHandler of this.#cancelHandlers) { + cancelHandler(); + } + } catch (error) { + console.warn('Cancellation threw an error', error); + return; + } + } + this.#cancelHandlers.length = 0; + if (this.#reject) this.#reject(new CancelError('Request aborted')); + } + + public get isCancelled(): boolean { + return this.#isCancelled; + } +} diff --git a/modules/frontend/src/api/core/OpenAPI.ts b/modules/frontend/src/api/core/OpenAPI.ts new file mode 100644 index 0000000..185e5c3 --- /dev/null +++ b/modules/frontend/src/api/core/OpenAPI.ts @@ -0,0 +1,32 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { ApiRequestOptions } from './ApiRequestOptions'; + +type Resolver = (options: ApiRequestOptions) => Promise; +type Headers = Record; + +export type OpenAPIConfig = { + BASE: string; + VERSION: string; + WITH_CREDENTIALS: boolean; + CREDENTIALS: 'include' | 'omit' | 'same-origin'; + TOKEN?: string | Resolver | undefined; + USERNAME?: string | Resolver | undefined; + PASSWORD?: string | Resolver | undefined; + HEADERS?: Headers | Resolver | undefined; + ENCODE_PATH?: ((path: string) => string) | undefined; +}; + +export const OpenAPI: OpenAPIConfig = { + BASE: '/api/v1', + VERSION: '1.0.0', + WITH_CREDENTIALS: false, + CREDENTIALS: 'include', + TOKEN: undefined, + USERNAME: undefined, + PASSWORD: undefined, + HEADERS: undefined, + ENCODE_PATH: undefined, +}; diff --git a/modules/frontend/src/api/core/auth.gen.ts b/modules/frontend/src/api/core/auth.gen.ts deleted file mode 100644 index f8a7326..0000000 --- a/modules/frontend/src/api/core/auth.gen.ts +++ /dev/null @@ -1,42 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -export type AuthToken = string | undefined; - -export interface Auth { - /** - * Which part of the request do we use to send the auth? - * - * @default 'header' - */ - in?: 'header' | 'query' | 'cookie'; - /** - * Header or query parameter name. - * - * @default 'Authorization' - */ - name?: string; - scheme?: 'basic' | 'bearer'; - type: 'apiKey' | 'http'; -} - -export const getAuthToken = async ( - auth: Auth, - callback: ((auth: Auth) => Promise | AuthToken) | AuthToken, -): Promise => { - const token = - typeof callback === 'function' ? await callback(auth) : callback; - - if (!token) { - return; - } - - if (auth.scheme === 'bearer') { - return `Bearer ${token}`; - } - - if (auth.scheme === 'basic') { - return `Basic ${btoa(token)}`; - } - - return token; -}; diff --git a/modules/frontend/src/api/core/bodySerializer.gen.ts b/modules/frontend/src/api/core/bodySerializer.gen.ts deleted file mode 100644 index 552b50f..0000000 --- a/modules/frontend/src/api/core/bodySerializer.gen.ts +++ /dev/null @@ -1,100 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { - ArrayStyle, - ObjectStyle, - SerializerOptions, -} from './pathSerializer.gen'; - -export type QuerySerializer = (query: Record) => string; - -export type BodySerializer = (body: any) => any; - -type QuerySerializerOptionsObject = { - allowReserved?: boolean; - array?: Partial>; - object?: Partial>; -}; - -export type QuerySerializerOptions = QuerySerializerOptionsObject & { - /** - * Per-parameter serialization overrides. When provided, these settings - * override the global array/object settings for specific parameter names. - */ - parameters?: Record; -}; - -const serializeFormDataPair = ( - data: FormData, - key: string, - value: unknown, -): void => { - if (typeof value === 'string' || value instanceof Blob) { - data.append(key, value); - } else if (value instanceof Date) { - data.append(key, value.toISOString()); - } else { - data.append(key, JSON.stringify(value)); - } -}; - -const serializeUrlSearchParamsPair = ( - data: URLSearchParams, - key: string, - value: unknown, -): void => { - if (typeof value === 'string') { - data.append(key, value); - } else { - data.append(key, JSON.stringify(value)); - } -}; - -export const formDataBodySerializer = { - bodySerializer: | Array>>( - body: T, - ): FormData => { - const data = new FormData(); - - Object.entries(body).forEach(([key, value]) => { - if (value === undefined || value === null) { - return; - } - if (Array.isArray(value)) { - value.forEach((v) => serializeFormDataPair(data, key, v)); - } else { - serializeFormDataPair(data, key, value); - } - }); - - return data; - }, -}; - -export const jsonBodySerializer = { - bodySerializer: (body: T): string => - JSON.stringify(body, (_key, value) => - typeof value === 'bigint' ? value.toString() : value, - ), -}; - -export const urlSearchParamsBodySerializer = { - bodySerializer: | Array>>( - body: T, - ): string => { - const data = new URLSearchParams(); - - Object.entries(body).forEach(([key, value]) => { - if (value === undefined || value === null) { - return; - } - if (Array.isArray(value)) { - value.forEach((v) => serializeUrlSearchParamsPair(data, key, v)); - } else { - serializeUrlSearchParamsPair(data, key, value); - } - }); - - return data.toString(); - }, -}; diff --git a/modules/frontend/src/api/core/params.gen.ts b/modules/frontend/src/api/core/params.gen.ts deleted file mode 100644 index 602715c..0000000 --- a/modules/frontend/src/api/core/params.gen.ts +++ /dev/null @@ -1,176 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -type Slot = 'body' | 'headers' | 'path' | 'query'; - -export type Field = - | { - in: Exclude; - /** - * Field name. This is the name we want the user to see and use. - */ - key: string; - /** - * Field mapped name. This is the name we want to use in the request. - * If omitted, we use the same value as `key`. - */ - map?: string; - } - | { - in: Extract; - /** - * Key isn't required for bodies. - */ - key?: string; - map?: string; - } - | { - /** - * Field name. This is the name we want the user to see and use. - */ - key: string; - /** - * Field mapped name. This is the name we want to use in the request. - * If `in` is omitted, `map` aliases `key` to the transport layer. - */ - map: Slot; - }; - -export interface Fields { - allowExtra?: Partial>; - args?: ReadonlyArray; -} - -export type FieldsConfig = ReadonlyArray; - -const extraPrefixesMap: Record = { - $body_: 'body', - $headers_: 'headers', - $path_: 'path', - $query_: 'query', -}; -const extraPrefixes = Object.entries(extraPrefixesMap); - -type KeyMap = Map< - string, - | { - in: Slot; - map?: string; - } - | { - in?: never; - map: Slot; - } ->; - -const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => { - if (!map) { - map = new Map(); - } - - for (const config of fields) { - if ('in' in config) { - if (config.key) { - map.set(config.key, { - in: config.in, - map: config.map, - }); - } - } else if ('key' in config) { - map.set(config.key, { - map: config.map, - }); - } else if (config.args) { - buildKeyMap(config.args, map); - } - } - - return map; -}; - -interface Params { - body: unknown; - headers: Record; - path: Record; - query: Record; -} - -const stripEmptySlots = (params: Params) => { - for (const [slot, value] of Object.entries(params)) { - if (value && typeof value === 'object' && !Object.keys(value).length) { - delete params[slot as Slot]; - } - } -}; - -export const buildClientParams = ( - args: ReadonlyArray, - fields: FieldsConfig, -) => { - const params: Params = { - body: {}, - headers: {}, - path: {}, - query: {}, - }; - - const map = buildKeyMap(fields); - - let config: FieldsConfig[number] | undefined; - - for (const [index, arg] of args.entries()) { - if (fields[index]) { - config = fields[index]; - } - - if (!config) { - continue; - } - - if ('in' in config) { - if (config.key) { - const field = map.get(config.key)!; - const name = field.map || config.key; - if (field.in) { - (params[field.in] as Record)[name] = arg; - } - } else { - params.body = arg; - } - } else { - for (const [key, value] of Object.entries(arg ?? {})) { - const field = map.get(key); - - if (field) { - if (field.in) { - const name = field.map || key; - (params[field.in] as Record)[name] = value; - } else { - params[field.map] = value; - } - } else { - const extra = extraPrefixes.find(([prefix]) => - key.startsWith(prefix), - ); - - if (extra) { - const [prefix, slot] = extra; - (params[slot] as Record)[ - key.slice(prefix.length) - ] = value; - } else if ('allowExtra' in config && config.allowExtra) { - for (const [slot, allowed] of Object.entries(config.allowExtra)) { - if (allowed) { - (params[slot as Slot] as Record)[key] = value; - break; - } - } - } - } - } - } - } - - stripEmptySlots(params); - - return params; -}; diff --git a/modules/frontend/src/api/core/pathSerializer.gen.ts b/modules/frontend/src/api/core/pathSerializer.gen.ts deleted file mode 100644 index 8d99931..0000000 --- a/modules/frontend/src/api/core/pathSerializer.gen.ts +++ /dev/null @@ -1,181 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -interface SerializeOptions - extends SerializePrimitiveOptions, - SerializerOptions {} - -interface SerializePrimitiveOptions { - allowReserved?: boolean; - name: string; -} - -export interface SerializerOptions { - /** - * @default true - */ - explode: boolean; - style: T; -} - -export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; -export type ArraySeparatorStyle = ArrayStyle | MatrixStyle; -type MatrixStyle = 'label' | 'matrix' | 'simple'; -export type ObjectStyle = 'form' | 'deepObject'; -type ObjectSeparatorStyle = ObjectStyle | MatrixStyle; - -interface SerializePrimitiveParam extends SerializePrimitiveOptions { - value: string; -} - -export const separatorArrayExplode = (style: ArraySeparatorStyle) => { - switch (style) { - case 'label': - return '.'; - case 'matrix': - return ';'; - case 'simple': - return ','; - default: - return '&'; - } -}; - -export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => { - switch (style) { - case 'form': - return ','; - case 'pipeDelimited': - return '|'; - case 'spaceDelimited': - return '%20'; - default: - return ','; - } -}; - -export const separatorObjectExplode = (style: ObjectSeparatorStyle) => { - switch (style) { - case 'label': - return '.'; - case 'matrix': - return ';'; - case 'simple': - return ','; - default: - return '&'; - } -}; - -export const serializeArrayParam = ({ - allowReserved, - explode, - name, - style, - value, -}: SerializeOptions & { - value: unknown[]; -}) => { - if (!explode) { - const joinedValues = ( - allowReserved ? value : value.map((v) => encodeURIComponent(v as string)) - ).join(separatorArrayNoExplode(style)); - switch (style) { - case 'label': - return `.${joinedValues}`; - case 'matrix': - return `;${name}=${joinedValues}`; - case 'simple': - return joinedValues; - default: - return `${name}=${joinedValues}`; - } - } - - const separator = separatorArrayExplode(style); - const joinedValues = value - .map((v) => { - if (style === 'label' || style === 'simple') { - return allowReserved ? v : encodeURIComponent(v as string); - } - - return serializePrimitiveParam({ - allowReserved, - name, - value: v as string, - }); - }) - .join(separator); - return style === 'label' || style === 'matrix' - ? separator + joinedValues - : joinedValues; -}; - -export const serializePrimitiveParam = ({ - allowReserved, - name, - value, -}: SerializePrimitiveParam) => { - if (value === undefined || value === null) { - return ''; - } - - if (typeof value === 'object') { - throw new Error( - 'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.', - ); - } - - return `${name}=${allowReserved ? value : encodeURIComponent(value)}`; -}; - -export const serializeObjectParam = ({ - allowReserved, - explode, - name, - style, - value, - valueOnly, -}: SerializeOptions & { - value: Record | Date; - valueOnly?: boolean; -}) => { - if (value instanceof Date) { - return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`; - } - - if (style !== 'deepObject' && !explode) { - let values: string[] = []; - Object.entries(value).forEach(([key, v]) => { - values = [ - ...values, - key, - allowReserved ? (v as string) : encodeURIComponent(v as string), - ]; - }); - const joinedValues = values.join(','); - switch (style) { - case 'form': - return `${name}=${joinedValues}`; - case 'label': - return `.${joinedValues}`; - case 'matrix': - return `;${name}=${joinedValues}`; - default: - return joinedValues; - } - } - - const separator = separatorObjectExplode(style); - const joinedValues = Object.entries(value) - .map(([key, v]) => - serializePrimitiveParam({ - allowReserved, - name: style === 'deepObject' ? `${name}[${key}]` : key, - value: v as string, - }), - ) - .join(separator); - return style === 'label' || style === 'matrix' - ? separator + joinedValues - : joinedValues; -}; diff --git a/modules/frontend/src/api/core/queryKeySerializer.gen.ts b/modules/frontend/src/api/core/queryKeySerializer.gen.ts deleted file mode 100644 index d3bb683..0000000 --- a/modules/frontend/src/api/core/queryKeySerializer.gen.ts +++ /dev/null @@ -1,136 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -/** - * JSON-friendly union that mirrors what Pinia Colada can hash. - */ -export type JsonValue = - | null - | string - | number - | boolean - | JsonValue[] - | { [key: string]: JsonValue }; - -/** - * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. - */ -export const queryKeyJsonReplacer = (_key: string, value: unknown) => { - if ( - value === undefined || - typeof value === 'function' || - typeof value === 'symbol' - ) { - return undefined; - } - if (typeof value === 'bigint') { - return value.toString(); - } - if (value instanceof Date) { - return value.toISOString(); - } - return value; -}; - -/** - * Safely stringifies a value and parses it back into a JsonValue. - */ -export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => { - try { - const json = JSON.stringify(input, queryKeyJsonReplacer); - if (json === undefined) { - return undefined; - } - return JSON.parse(json) as JsonValue; - } catch { - return undefined; - } -}; - -/** - * Detects plain objects (including objects with a null prototype). - */ -const isPlainObject = (value: unknown): value is Record => { - if (value === null || typeof value !== 'object') { - return false; - } - const prototype = Object.getPrototypeOf(value as object); - return prototype === Object.prototype || prototype === null; -}; - -/** - * Turns URLSearchParams into a sorted JSON object for deterministic keys. - */ -const serializeSearchParams = (params: URLSearchParams): JsonValue => { - const entries = Array.from(params.entries()).sort(([a], [b]) => - a.localeCompare(b), - ); - const result: Record = {}; - - for (const [key, value] of entries) { - const existing = result[key]; - if (existing === undefined) { - result[key] = value; - continue; - } - - if (Array.isArray(existing)) { - (existing as string[]).push(value); - } else { - result[key] = [existing, value]; - } - } - - return result; -}; - -/** - * Normalizes any accepted value into a JSON-friendly shape for query keys. - */ -export const serializeQueryKeyValue = ( - value: unknown, -): JsonValue | undefined => { - if (value === null) { - return null; - } - - if ( - typeof value === 'string' || - typeof value === 'number' || - typeof value === 'boolean' - ) { - return value; - } - - if ( - value === undefined || - typeof value === 'function' || - typeof value === 'symbol' - ) { - return undefined; - } - - if (typeof value === 'bigint') { - return value.toString(); - } - - if (value instanceof Date) { - return value.toISOString(); - } - - if (Array.isArray(value)) { - return stringifyToJsonValue(value); - } - - if ( - typeof URLSearchParams !== 'undefined' && - value instanceof URLSearchParams - ) { - return serializeSearchParams(value); - } - - if (isPlainObject(value)) { - return stringifyToJsonValue(value); - } - - return undefined; -}; diff --git a/modules/frontend/src/api/core/request.ts b/modules/frontend/src/api/core/request.ts new file mode 100644 index 0000000..1dc6fef --- /dev/null +++ b/modules/frontend/src/api/core/request.ts @@ -0,0 +1,323 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import axios from 'axios'; +import type { AxiosError, AxiosRequestConfig, AxiosResponse, AxiosInstance } from 'axios'; +import FormData from 'form-data'; + +import { ApiError } from './ApiError'; +import type { ApiRequestOptions } from './ApiRequestOptions'; +import type { ApiResult } from './ApiResult'; +import { CancelablePromise } from './CancelablePromise'; +import type { OnCancel } from './CancelablePromise'; +import type { OpenAPIConfig } from './OpenAPI'; + +export const isDefined = (value: T | null | undefined): value is Exclude => { + return value !== undefined && value !== null; +}; + +export const isString = (value: any): value is string => { + return typeof value === 'string'; +}; + +export const isStringWithValue = (value: any): value is string => { + return isString(value) && value !== ''; +}; + +export const isBlob = (value: any): value is Blob => { + return ( + typeof value === 'object' && + typeof value.type === 'string' && + typeof value.stream === 'function' && + typeof value.arrayBuffer === 'function' && + typeof value.constructor === 'function' && + typeof value.constructor.name === 'string' && + /^(Blob|File)$/.test(value.constructor.name) && + /^(Blob|File)$/.test(value[Symbol.toStringTag]) + ); +}; + +export const isFormData = (value: any): value is FormData => { + return value instanceof FormData; +}; + +export const isSuccess = (status: number): boolean => { + return status >= 200 && status < 300; +}; + +export const base64 = (str: string): string => { + try { + return btoa(str); + } catch (err) { + // @ts-ignore + return Buffer.from(str).toString('base64'); + } +}; + +export const getQueryString = (params: Record): string => { + const qs: string[] = []; + + const append = (key: string, value: any) => { + qs.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`); + }; + + const process = (key: string, value: any) => { + if (isDefined(value)) { + if (Array.isArray(value)) { + value.forEach(v => { + process(key, v); + }); + } else if (typeof value === 'object') { + Object.entries(value).forEach(([k, v]) => { + process(`${key}[${k}]`, v); + }); + } else { + append(key, value); + } + } + }; + + Object.entries(params).forEach(([key, value]) => { + process(key, value); + }); + + if (qs.length > 0) { + return `?${qs.join('&')}`; + } + + return ''; +}; + +const getUrl = (config: OpenAPIConfig, options: ApiRequestOptions): string => { + const encoder = config.ENCODE_PATH || encodeURI; + + const path = options.url + .replace('{api-version}', config.VERSION) + .replace(/{(.*?)}/g, (substring: string, group: string) => { + if (options.path?.hasOwnProperty(group)) { + return encoder(String(options.path[group])); + } + return substring; + }); + + const url = `${config.BASE}${path}`; + if (options.query) { + return `${url}${getQueryString(options.query)}`; + } + return url; +}; + +export const getFormData = (options: ApiRequestOptions): FormData | undefined => { + if (options.formData) { + const formData = new FormData(); + + const process = (key: string, value: any) => { + if (isString(value) || isBlob(value)) { + formData.append(key, value); + } else { + formData.append(key, JSON.stringify(value)); + } + }; + + Object.entries(options.formData) + .filter(([_, value]) => isDefined(value)) + .forEach(([key, value]) => { + if (Array.isArray(value)) { + value.forEach(v => process(key, v)); + } else { + process(key, value); + } + }); + + return formData; + } + return undefined; +}; + +type Resolver = (options: ApiRequestOptions) => Promise; + +export const resolve = async (options: ApiRequestOptions, resolver?: T | Resolver): Promise => { + if (typeof resolver === 'function') { + return (resolver as Resolver)(options); + } + return resolver; +}; + +export const getHeaders = async (config: OpenAPIConfig, options: ApiRequestOptions, formData?: FormData): Promise> => { + const [token, username, password, additionalHeaders] = await Promise.all([ + resolve(options, config.TOKEN), + resolve(options, config.USERNAME), + resolve(options, config.PASSWORD), + resolve(options, config.HEADERS), + ]); + + const formHeaders = typeof formData?.getHeaders === 'function' && formData?.getHeaders() || {} + + const headers = Object.entries({ + Accept: 'application/json', + ...additionalHeaders, + ...options.headers, + ...formHeaders, + }) + .filter(([_, value]) => isDefined(value)) + .reduce((headers, [key, value]) => ({ + ...headers, + [key]: String(value), + }), {} as Record); + + if (isStringWithValue(token)) { + headers['Authorization'] = `Bearer ${token}`; + } + + if (isStringWithValue(username) && isStringWithValue(password)) { + const credentials = base64(`${username}:${password}`); + headers['Authorization'] = `Basic ${credentials}`; + } + + if (options.body !== undefined) { + if (options.mediaType) { + headers['Content-Type'] = options.mediaType; + } else if (isBlob(options.body)) { + headers['Content-Type'] = options.body.type || 'application/octet-stream'; + } else if (isString(options.body)) { + headers['Content-Type'] = 'text/plain'; + } else if (!isFormData(options.body)) { + headers['Content-Type'] = 'application/json'; + } + } + + return headers; +}; + +export const getRequestBody = (options: ApiRequestOptions): any => { + if (options.body) { + return options.body; + } + return undefined; +}; + +export const sendRequest = async ( + config: OpenAPIConfig, + options: ApiRequestOptions, + url: string, + body: any, + formData: FormData | undefined, + headers: Record, + onCancel: OnCancel, + axiosClient: AxiosInstance +): Promise> => { + const source = axios.CancelToken.source(); + + const requestConfig: AxiosRequestConfig = { + url, + headers, + data: body ?? formData, + method: options.method, + withCredentials: config.WITH_CREDENTIALS, + withXSRFToken: config.CREDENTIALS === 'include' ? config.WITH_CREDENTIALS : false, + cancelToken: source.token, + }; + + onCancel(() => source.cancel('The user aborted a request.')); + + try { + return await axiosClient.request(requestConfig); + } catch (error) { + const axiosError = error as AxiosError; + if (axiosError.response) { + return axiosError.response; + } + throw error; + } +}; + +export const getResponseHeader = (response: AxiosResponse, responseHeader?: string): string | undefined => { + if (responseHeader) { + const content = response.headers[responseHeader]; + if (isString(content)) { + return content; + } + } + return undefined; +}; + +export const getResponseBody = (response: AxiosResponse): any => { + if (response.status !== 204) { + return response.data; + } + return undefined; +}; + +export const catchErrorCodes = (options: ApiRequestOptions, result: ApiResult): void => { + const errors: Record = { + 400: 'Bad Request', + 401: 'Unauthorized', + 403: 'Forbidden', + 404: 'Not Found', + 500: 'Internal Server Error', + 502: 'Bad Gateway', + 503: 'Service Unavailable', + ...options.errors, + } + + const error = errors[result.status]; + if (error) { + throw new ApiError(options, result, error); + } + + if (!result.ok) { + const errorStatus = result.status ?? 'unknown'; + const errorStatusText = result.statusText ?? 'unknown'; + const errorBody = (() => { + try { + return JSON.stringify(result.body, null, 2); + } catch (e) { + return undefined; + } + })(); + + throw new ApiError(options, result, + `Generic Error: status: ${errorStatus}; status text: ${errorStatusText}; body: ${errorBody}` + ); + } +}; + +/** + * Request method + * @param config The OpenAPI configuration object + * @param options The request options from the service + * @param axiosClient The axios client instance to use + * @returns CancelablePromise + * @throws ApiError + */ +export const request = (config: OpenAPIConfig, options: ApiRequestOptions, axiosClient: AxiosInstance = axios): CancelablePromise => { + return new CancelablePromise(async (resolve, reject, onCancel) => { + try { + const url = getUrl(config, options); + const formData = getFormData(options); + const body = getRequestBody(options); + const headers = await getHeaders(config, options, formData); + + if (!onCancel.isCancelled) { + const response = await sendRequest(config, options, url, body, formData, headers, onCancel, axiosClient); + const responseBody = getResponseBody(response); + const responseHeader = getResponseHeader(response, options.responseHeader); + + const result: ApiResult = { + url, + ok: isSuccess(response.status), + status: response.status, + statusText: response.statusText, + body: responseHeader ?? responseBody, + }; + + catchErrorCodes(options, result); + + resolve(result.body); + } + } catch (error) { + reject(error); + } + }); +}; diff --git a/modules/frontend/src/api/core/serverSentEvents.gen.ts b/modules/frontend/src/api/core/serverSentEvents.gen.ts deleted file mode 100644 index f8fd78e..0000000 --- a/modules/frontend/src/api/core/serverSentEvents.gen.ts +++ /dev/null @@ -1,264 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { Config } from './types.gen'; - -export type ServerSentEventsOptions = Omit< - RequestInit, - 'method' -> & - Pick & { - /** - * Fetch API implementation. You can use this option to provide a custom - * fetch instance. - * - * @default globalThis.fetch - */ - fetch?: typeof fetch; - /** - * Implementing clients can call request interceptors inside this hook. - */ - onRequest?: (url: string, init: RequestInit) => Promise; - /** - * Callback invoked when a network or parsing error occurs during streaming. - * - * This option applies only if the endpoint returns a stream of events. - * - * @param error The error that occurred. - */ - onSseError?: (error: unknown) => void; - /** - * Callback invoked when an event is streamed from the server. - * - * This option applies only if the endpoint returns a stream of events. - * - * @param event Event streamed from the server. - * @returns Nothing (void). - */ - onSseEvent?: (event: StreamEvent) => void; - serializedBody?: RequestInit['body']; - /** - * Default retry delay in milliseconds. - * - * This option applies only if the endpoint returns a stream of events. - * - * @default 3000 - */ - sseDefaultRetryDelay?: number; - /** - * Maximum number of retry attempts before giving up. - */ - sseMaxRetryAttempts?: number; - /** - * Maximum retry delay in milliseconds. - * - * Applies only when exponential backoff is used. - * - * This option applies only if the endpoint returns a stream of events. - * - * @default 30000 - */ - sseMaxRetryDelay?: number; - /** - * Optional sleep function for retry backoff. - * - * Defaults to using `setTimeout`. - */ - sseSleepFn?: (ms: number) => Promise; - url: string; - }; - -export interface StreamEvent { - data: TData; - event?: string; - id?: string; - retry?: number; -} - -export type ServerSentEventsResult< - TData = unknown, - TReturn = void, - TNext = unknown, -> = { - stream: AsyncGenerator< - TData extends Record ? TData[keyof TData] : TData, - TReturn, - TNext - >; -}; - -export const createSseClient = ({ - onRequest, - onSseError, - onSseEvent, - responseTransformer, - responseValidator, - sseDefaultRetryDelay, - sseMaxRetryAttempts, - sseMaxRetryDelay, - sseSleepFn, - url, - ...options -}: ServerSentEventsOptions): ServerSentEventsResult => { - let lastEventId: string | undefined; - - const sleep = - sseSleepFn ?? - ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))); - - const createStream = async function* () { - let retryDelay: number = sseDefaultRetryDelay ?? 3000; - let attempt = 0; - const signal = options.signal ?? new AbortController().signal; - - while (true) { - if (signal.aborted) break; - - attempt++; - - const headers = - options.headers instanceof Headers - ? options.headers - : new Headers(options.headers as Record | undefined); - - if (lastEventId !== undefined) { - headers.set('Last-Event-ID', lastEventId); - } - - try { - const requestInit: RequestInit = { - redirect: 'follow', - ...options, - body: options.serializedBody, - headers, - signal, - }; - let request = new Request(url, requestInit); - if (onRequest) { - request = await onRequest(url, requestInit); - } - // fetch must be assigned here, otherwise it would throw the error: - // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation - const _fetch = options.fetch ?? globalThis.fetch; - const response = await _fetch(request); - - if (!response.ok) - throw new Error( - `SSE failed: ${response.status} ${response.statusText}`, - ); - - if (!response.body) throw new Error('No body in SSE response'); - - const reader = response.body - .pipeThrough(new TextDecoderStream()) - .getReader(); - - let buffer = ''; - - const abortHandler = () => { - try { - reader.cancel(); - } catch { - // noop - } - }; - - signal.addEventListener('abort', abortHandler); - - try { - while (true) { - const { done, value } = await reader.read(); - if (done) break; - buffer += value; - - const chunks = buffer.split('\n\n'); - buffer = chunks.pop() ?? ''; - - for (const chunk of chunks) { - const lines = chunk.split('\n'); - const dataLines: Array = []; - let eventName: string | undefined; - - for (const line of lines) { - if (line.startsWith('data:')) { - dataLines.push(line.replace(/^data:\s*/, '')); - } else if (line.startsWith('event:')) { - eventName = line.replace(/^event:\s*/, ''); - } else if (line.startsWith('id:')) { - lastEventId = line.replace(/^id:\s*/, ''); - } else if (line.startsWith('retry:')) { - const parsed = Number.parseInt( - line.replace(/^retry:\s*/, ''), - 10, - ); - if (!Number.isNaN(parsed)) { - retryDelay = parsed; - } - } - } - - let data: unknown; - let parsedJson = false; - - if (dataLines.length) { - const rawData = dataLines.join('\n'); - try { - data = JSON.parse(rawData); - parsedJson = true; - } catch { - data = rawData; - } - } - - if (parsedJson) { - if (responseValidator) { - await responseValidator(data); - } - - if (responseTransformer) { - data = await responseTransformer(data); - } - } - - onSseEvent?.({ - data, - event: eventName, - id: lastEventId, - retry: retryDelay, - }); - - if (dataLines.length) { - yield data as any; - } - } - } - } finally { - signal.removeEventListener('abort', abortHandler); - reader.releaseLock(); - } - - break; // exit loop on normal completion - } catch (error) { - // connection failed or aborted; retry after delay - onSseError?.(error); - - if ( - sseMaxRetryAttempts !== undefined && - attempt >= sseMaxRetryAttempts - ) { - break; // stop after firing error - } - - // exponential backoff: double retry each attempt, cap at 30s - const backoff = Math.min( - retryDelay * 2 ** (attempt - 1), - sseMaxRetryDelay ?? 30000, - ); - await sleep(backoff); - } - } - }; - - const stream = createStream(); - - return { stream }; -}; diff --git a/modules/frontend/src/api/core/types.gen.ts b/modules/frontend/src/api/core/types.gen.ts deleted file mode 100644 index 643c070..0000000 --- a/modules/frontend/src/api/core/types.gen.ts +++ /dev/null @@ -1,118 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { Auth, AuthToken } from './auth.gen'; -import type { - BodySerializer, - QuerySerializer, - QuerySerializerOptions, -} from './bodySerializer.gen'; - -export type HttpMethod = - | 'connect' - | 'delete' - | 'get' - | 'head' - | 'options' - | 'patch' - | 'post' - | 'put' - | 'trace'; - -export type Client< - RequestFn = never, - Config = unknown, - MethodFn = never, - BuildUrlFn = never, - SseFn = never, -> = { - /** - * Returns the final request URL. - */ - buildUrl: BuildUrlFn; - getConfig: () => Config; - request: RequestFn; - setConfig: (config: Config) => Config; -} & { - [K in HttpMethod]: MethodFn; -} & ([SseFn] extends [never] - ? { sse?: never } - : { sse: { [K in HttpMethod]: SseFn } }); - -export interface Config { - /** - * Auth token or a function returning auth token. The resolved value will be - * added to the request payload as defined by its `security` array. - */ - auth?: ((auth: Auth) => Promise | AuthToken) | AuthToken; - /** - * A function for serializing request body parameter. By default, - * {@link JSON.stringify()} will be used. - */ - bodySerializer?: BodySerializer | null; - /** - * An object containing any HTTP headers that you want to pre-populate your - * `Headers` object with. - * - * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} - */ - headers?: - | RequestInit['headers'] - | Record< - string, - | string - | number - | boolean - | (string | number | boolean)[] - | null - | undefined - | unknown - >; - /** - * The request method. - * - * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} - */ - method?: Uppercase; - /** - * A function for serializing request query parameters. By default, arrays - * will be exploded in form style, objects will be exploded in deepObject - * style, and reserved characters are percent-encoded. - * - * This method will have no effect if the native `paramsSerializer()` Axios - * API function is used. - * - * {@link https://swagger.io/docs/specification/serialization/#query View examples} - */ - querySerializer?: QuerySerializer | QuerySerializerOptions; - /** - * A function validating request data. This is useful if you want to ensure - * the request conforms to the desired shape, so it can be safely sent to - * the server. - */ - requestValidator?: (data: unknown) => Promise; - /** - * A function transforming response data before it's returned. This is useful - * for post-processing data, e.g. converting ISO strings into Date objects. - */ - responseTransformer?: (data: unknown) => Promise; - /** - * A function validating response data. This is useful if you want to ensure - * the response conforms to the desired shape, so it can be safely passed to - * the transformers and returned to the user. - */ - responseValidator?: (data: unknown) => Promise; -} - -type IsExactlyNeverOrNeverUndefined = [T] extends [never] - ? true - : [T] extends [never | undefined] - ? [undefined] extends [T] - ? false - : true - : false; - -export type OmitNever> = { - [K in keyof T as IsExactlyNeverOrNeverUndefined extends true - ? never - : K]: T[K]; -}; diff --git a/modules/frontend/src/api/core/utils.gen.ts b/modules/frontend/src/api/core/utils.gen.ts deleted file mode 100644 index 0b5389d..0000000 --- a/modules/frontend/src/api/core/utils.gen.ts +++ /dev/null @@ -1,143 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { BodySerializer, QuerySerializer } from './bodySerializer.gen'; -import { - type ArraySeparatorStyle, - serializeArrayParam, - serializeObjectParam, - serializePrimitiveParam, -} from './pathSerializer.gen'; - -export interface PathSerializer { - path: Record; - url: string; -} - -export const PATH_PARAM_RE = /\{[^{}]+\}/g; - -export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { - let url = _url; - const matches = _url.match(PATH_PARAM_RE); - if (matches) { - for (const match of matches) { - let explode = false; - let name = match.substring(1, match.length - 1); - let style: ArraySeparatorStyle = 'simple'; - - if (name.endsWith('*')) { - explode = true; - name = name.substring(0, name.length - 1); - } - - if (name.startsWith('.')) { - name = name.substring(1); - style = 'label'; - } else if (name.startsWith(';')) { - name = name.substring(1); - style = 'matrix'; - } - - const value = path[name]; - - if (value === undefined || value === null) { - continue; - } - - if (Array.isArray(value)) { - url = url.replace( - match, - serializeArrayParam({ explode, name, style, value }), - ); - continue; - } - - if (typeof value === 'object') { - url = url.replace( - match, - serializeObjectParam({ - explode, - name, - style, - value: value as Record, - valueOnly: true, - }), - ); - continue; - } - - if (style === 'matrix') { - url = url.replace( - match, - `;${serializePrimitiveParam({ - name, - value: value as string, - })}`, - ); - continue; - } - - const replaceValue = encodeURIComponent( - style === 'label' ? `.${value as string}` : (value as string), - ); - url = url.replace(match, replaceValue); - } - } - return url; -}; - -export const getUrl = ({ - baseUrl, - path, - query, - querySerializer, - url: _url, -}: { - baseUrl?: string; - path?: Record; - query?: Record; - querySerializer: QuerySerializer; - url: string; -}) => { - const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; - let url = (baseUrl ?? '') + pathUrl; - if (path) { - url = defaultPathSerializer({ path, url }); - } - let search = query ? querySerializer(query) : ''; - if (search.startsWith('?')) { - search = search.substring(1); - } - if (search) { - url += `?${search}`; - } - return url; -}; - -export function getValidRequestBody(options: { - body?: unknown; - bodySerializer?: BodySerializer | null; - serializedBody?: unknown; -}) { - const hasBody = options.body !== undefined; - const isSerializedBody = hasBody && options.bodySerializer; - - if (isSerializedBody) { - if ('serializedBody' in options) { - const hasSerializedBody = - options.serializedBody !== undefined && options.serializedBody !== ''; - - return hasSerializedBody ? options.serializedBody : null; - } - - // not all clients implement a serializedBody property (i.e. client-axios) - return options.body !== '' ? options.body : null; - } - - // plain/text body - if (hasBody) { - return options.body; - } - - // no body was provided - return undefined; -} diff --git a/modules/frontend/src/api/index.ts b/modules/frontend/src/api/index.ts index c352c10..9013fc7 100644 --- a/modules/frontend/src/api/index.ts +++ b/modules/frontend/src/api/index.ts @@ -1,4 +1,28 @@ -// This file is auto-generated by @hey-api/openapi-ts +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export { ApiError } from './core/ApiError'; +export { CancelablePromise, CancelError } from './core/CancelablePromise'; +export { OpenAPI } from './core/OpenAPI'; +export type { OpenAPIConfig } from './core/OpenAPI'; -export type * from './types.gen'; -export * from './sdk.gen'; +export type { cursor } from './models/cursor'; +export type { CursorObj } from './models/CursorObj'; +export type { Image } from './models/Image'; +export type { ReleaseSeason } from './models/ReleaseSeason'; +export type { Review } from './models/Review'; +export type { StorageType } from './models/StorageType'; +export type { Studio } from './models/Studio'; +export type { Tag } from './models/Tag'; +export type { Tags } from './models/Tags'; +export type { Title } from './models/Title'; +export type { title_sort } from './models/title_sort'; +export type { TitleSort } from './models/TitleSort'; +export type { TitleStatus } from './models/TitleStatus'; +export type { User } from './models/User'; +export type { UserTitle } from './models/UserTitle'; +export type { UserTitleMini } from './models/UserTitleMini'; +export type { UserTitleStatus } from './models/UserTitleStatus'; + +export { DefaultService } from './services/DefaultService'; diff --git a/modules/frontend/src/api/models/CursorObj.ts b/modules/frontend/src/api/models/CursorObj.ts new file mode 100644 index 0000000..f54abb1 --- /dev/null +++ b/modules/frontend/src/api/models/CursorObj.ts @@ -0,0 +1,9 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type CursorObj = { + id: number; + param?: string; +}; + diff --git a/modules/frontend/src/api/models/Image.ts b/modules/frontend/src/api/models/Image.ts new file mode 100644 index 0000000..887bf2f --- /dev/null +++ b/modules/frontend/src/api/models/Image.ts @@ -0,0 +1,11 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { StorageType } from './StorageType'; +export type Image = { + id?: number; + storage_type?: StorageType; + image_path?: string; +}; + diff --git a/modules/frontend/src/api/models/ReleaseSeason.ts b/modules/frontend/src/api/models/ReleaseSeason.ts new file mode 100644 index 0000000..ad9f930 --- /dev/null +++ b/modules/frontend/src/api/models/ReleaseSeason.ts @@ -0,0 +1,8 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +/** + * Title release season + */ +export type ReleaseSeason = 'winter' | 'spring' | 'summer' | 'fall'; diff --git a/modules/frontend/src/api/models/Review.ts b/modules/frontend/src/api/models/Review.ts new file mode 100644 index 0000000..9b453b7 --- /dev/null +++ b/modules/frontend/src/api/models/Review.ts @@ -0,0 +1,5 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type Review = Record; diff --git a/modules/frontend/src/api/models/StorageType.ts b/modules/frontend/src/api/models/StorageType.ts new file mode 100644 index 0000000..f6d086b --- /dev/null +++ b/modules/frontend/src/api/models/StorageType.ts @@ -0,0 +1,8 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +/** + * Image storage type + */ +export type StorageType = 's3' | 'local'; diff --git a/modules/frontend/src/api/models/Studio.ts b/modules/frontend/src/api/models/Studio.ts new file mode 100644 index 0000000..062695a --- /dev/null +++ b/modules/frontend/src/api/models/Studio.ts @@ -0,0 +1,12 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { Image } from './Image'; +export type Studio = { + id: number; + name: string; + poster?: Image; + description?: string; +}; + diff --git a/modules/frontend/src/api/models/Tag.ts b/modules/frontend/src/api/models/Tag.ts new file mode 100644 index 0000000..665c724 --- /dev/null +++ b/modules/frontend/src/api/models/Tag.ts @@ -0,0 +1,8 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +/** + * A localized tag: keys are language codes (ISO 639-1), values are tag names + */ +export type Tag = Record; diff --git a/modules/frontend/src/api/models/Tags.ts b/modules/frontend/src/api/models/Tags.ts new file mode 100644 index 0000000..748f066 --- /dev/null +++ b/modules/frontend/src/api/models/Tags.ts @@ -0,0 +1,9 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { Tag } from './Tag'; +/** + * Array of localized tags + */ +export type Tags = Array; diff --git a/modules/frontend/src/api/models/Title.ts b/modules/frontend/src/api/models/Title.ts new file mode 100644 index 0000000..9ffdeb6 --- /dev/null +++ b/modules/frontend/src/api/models/Title.ts @@ -0,0 +1,31 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { Image } from './Image'; +import type { ReleaseSeason } from './ReleaseSeason'; +import type { Studio } from './Studio'; +import type { Tags } from './Tags'; +import type { TitleStatus } from './TitleStatus'; +export type Title = { + /** + * Unique title ID (primary key) + */ + id: number; + /** + * Localized titles. Key = language (ISO 639-1), value = list of names + */ + title_names: Record>; + studio?: Studio; + tags: Tags; + poster?: Image; + title_status?: TitleStatus; + rating?: number; + rating_count?: number; + release_year?: number; + release_season?: ReleaseSeason; + episodes_aired?: number; + episodes_all?: number; + episodes_len?: Record; +}; + diff --git a/modules/frontend/src/api/models/TitleSort.ts b/modules/frontend/src/api/models/TitleSort.ts new file mode 100644 index 0000000..1c9385e --- /dev/null +++ b/modules/frontend/src/api/models/TitleSort.ts @@ -0,0 +1,8 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +/** + * Title sort order + */ +export type TitleSort = 'id' | 'year' | 'rating' | 'views'; diff --git a/modules/frontend/src/api/models/TitleStatus.ts b/modules/frontend/src/api/models/TitleStatus.ts new file mode 100644 index 0000000..72e0261 --- /dev/null +++ b/modules/frontend/src/api/models/TitleStatus.ts @@ -0,0 +1,8 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +/** + * Title status + */ +export type TitleStatus = 'finished' | 'ongoing' | 'planned'; diff --git a/modules/frontend/src/api/models/User.ts b/modules/frontend/src/api/models/User.ts new file mode 100644 index 0000000..969023f --- /dev/null +++ b/modules/frontend/src/api/models/User.ts @@ -0,0 +1,33 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { Image } from './Image'; +export type User = { + /** + * Unique user ID (primary key) + */ + id?: number; + image?: Image; + /** + * User email + */ + mail?: string; + /** + * Username (alphanumeric + _ or -) + */ + nickname: string; + /** + * Display name + */ + disp_name?: string; + /** + * User description + */ + user_desc?: string; + /** + * Timestamp when the user was created + */ + creation_date?: string; +}; + diff --git a/modules/frontend/src/api/models/UserTitle.ts b/modules/frontend/src/api/models/UserTitle.ts new file mode 100644 index 0000000..42b7919 --- /dev/null +++ b/modules/frontend/src/api/models/UserTitle.ts @@ -0,0 +1,15 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { Title } from './Title'; +import type { UserTitleStatus } from './UserTitleStatus'; +export type UserTitle = { + user_id: number; + title?: Title; + status: UserTitleStatus; + rate?: number; + review_id?: number; + ctime?: string; +}; + diff --git a/modules/frontend/src/api/models/UserTitleMini.ts b/modules/frontend/src/api/models/UserTitleMini.ts new file mode 100644 index 0000000..2b223ce --- /dev/null +++ b/modules/frontend/src/api/models/UserTitleMini.ts @@ -0,0 +1,14 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { UserTitleStatus } from './UserTitleStatus'; +export type UserTitleMini = { + user_id: number; + title_id: number; + status: UserTitleStatus; + rate?: number; + review_id?: number; + ctime?: string; +}; + diff --git a/modules/frontend/src/api/models/UserTitleStatus.ts b/modules/frontend/src/api/models/UserTitleStatus.ts new file mode 100644 index 0000000..0a29626 --- /dev/null +++ b/modules/frontend/src/api/models/UserTitleStatus.ts @@ -0,0 +1,8 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +/** + * User's title status + */ +export type UserTitleStatus = 'finished' | 'planned' | 'dropped' | 'in-progress'; diff --git a/modules/frontend/src/api/models/cursor.ts b/modules/frontend/src/api/models/cursor.ts new file mode 100644 index 0000000..5788e14 --- /dev/null +++ b/modules/frontend/src/api/models/cursor.ts @@ -0,0 +1,5 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type cursor = string; diff --git a/modules/frontend/src/api/models/title_sort.ts b/modules/frontend/src/api/models/title_sort.ts new file mode 100644 index 0000000..69b01a7 --- /dev/null +++ b/modules/frontend/src/api/models/title_sort.ts @@ -0,0 +1,6 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { TitleSort } from './TitleSort'; +export type title_sort = TitleSort; diff --git a/modules/frontend/src/api/sdk.gen.ts b/modules/frontend/src/api/sdk.gen.ts deleted file mode 100644 index 5359156..0000000 --- a/modules/frontend/src/api/sdk.gen.ts +++ /dev/null @@ -1,110 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { Client, Options as Options2, TDataShape } from './client'; -import { client } from './client.gen'; -import type { AddUserTitleData, AddUserTitleErrors, AddUserTitleResponses, DeleteUserTitleData, DeleteUserTitleErrors, DeleteUserTitleResponses, GetTitleData, GetTitleErrors, GetTitleResponses, GetTitlesData, GetTitlesErrors, GetTitlesResponses, GetUsersIdData, GetUsersIdErrors, GetUsersIdResponses, GetUserTitleData, GetUserTitleErrors, GetUserTitleResponses, GetUserTitlesData, GetUserTitlesErrors, GetUserTitlesResponses, UpdateUserData, UpdateUserErrors, UpdateUserResponses, UpdateUserTitleData, UpdateUserTitleErrors, UpdateUserTitleResponses } from './types.gen'; - -export type Options = Options2 & { - /** - * You can provide a client instance returned by `createClient()` instead of - * individual options. This might be also useful if you want to implement a - * custom client. - */ - client?: Client; - /** - * You can pass arbitrary values through the `meta` object. This can be - * used to access values that aren't defined as part of the SDK function. - */ - meta?: Record; -}; - -/** - * Get titles - */ -export const getTitles = (options?: Options) => (options?.client ?? client).get({ - querySerializer: { parameters: { status: { array: { explode: false } } } }, - url: '/titles', - ...options -}); - -/** - * Get title description - */ -export const getTitle = (options: Options) => (options.client ?? client).get({ url: '/titles/{title_id}', ...options }); - -/** - * Get user info - */ -export const getUsersId = (options: Options) => (options.client ?? client).get({ url: '/users/{user_id}', ...options }); - -/** - * Partially update a user account - * - * Update selected user profile fields (excluding password). - * Password updates must be done via the dedicated auth-service (`/auth/`). - * Fields not provided in the request body remain unchanged. - * - */ -export const updateUser = (options: Options) => (options.client ?? client).patch({ - security: [{ name: 'X-XSRF-TOKEN', type: 'apiKey' }], - url: '/users/{user_id}', - ...options, - headers: { - 'Content-Type': 'application/json', - ...options.headers - } -}); - -/** - * Get user titles - */ -export const getUserTitles = (options: Options) => (options.client ?? client).get({ - querySerializer: { parameters: { status: { array: { explode: false } }, watch_status: { array: { explode: false } } } }, - url: '/users/{user_id}/titles', - ...options -}); - -/** - * Add a title to a user - * - * User adding title to list af watched, status required - */ -export const addUserTitle = (options: Options) => (options.client ?? client).post({ - url: '/users/{user_id}/titles', - ...options, - headers: { - 'Content-Type': 'application/json', - ...options.headers - } -}); - -/** - * Delete a usertitle - * - * User deleting title from list of watched - */ -export const deleteUserTitle = (options: Options) => (options.client ?? client).delete({ - security: [{ name: 'X-XSRF-TOKEN', type: 'apiKey' }], - url: '/users/{user_id}/titles/{title_id}', - ...options -}); - -/** - * Get user title - */ -export const getUserTitle = (options: Options) => (options.client ?? client).get({ url: '/users/{user_id}/titles/{title_id}', ...options }); - -/** - * Update a usertitle - * - * User updating title list of watched - */ -export const updateUserTitle = (options: Options) => (options.client ?? client).patch({ - security: [{ name: 'X-XSRF-TOKEN', type: 'apiKey' }], - url: '/users/{user_id}/titles/{title_id}', - ...options, - headers: { - 'Content-Type': 'application/json', - ...options.headers - } -}); diff --git a/modules/frontend/src/api/services/DefaultService.ts b/modules/frontend/src/api/services/DefaultService.ts new file mode 100644 index 0000000..6898c46 --- /dev/null +++ b/modules/frontend/src/api/services/DefaultService.ts @@ -0,0 +1,371 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { CursorObj } from '../models/CursorObj'; +import type { ReleaseSeason } from '../models/ReleaseSeason'; +import type { Title } from '../models/Title'; +import type { TitleSort } from '../models/TitleSort'; +import type { TitleStatus } from '../models/TitleStatus'; +import type { User } from '../models/User'; +import type { UserTitle } from '../models/UserTitle'; +import type { UserTitleMini } from '../models/UserTitleMini'; +import type { UserTitleStatus } from '../models/UserTitleStatus'; +import type { CancelablePromise } from '../core/CancelablePromise'; +import { OpenAPI } from '../core/OpenAPI'; +import { request as __request } from '../core/request'; +export class DefaultService { + /** + * Get titles + * @param cursor + * @param sort + * @param sortForward + * @param extSearch + * @param word + * @param status List of title statuses to filter + * @param rating + * @param releaseYear + * @param releaseSeason + * @param limit + * @param offset + * @param fields + * @returns any List of titles with cursor + * @throws ApiError + */ + public static getTitles( + cursor?: string, + sort?: TitleSort, + sortForward: boolean = true, + extSearch: boolean = false, + word?: string, + status?: Array, + rating?: number, + releaseYear?: number, + releaseSeason?: ReleaseSeason, + limit: number = 10, + offset?: number, + fields: string = 'all', + ): CancelablePromise<{ + /** + * List of titles + */ + data: Array; + cursor: CursorObj; + }> { + return __request(OpenAPI, { + method: 'GET', + url: '/titles', + query: { + 'cursor': cursor, + 'sort': sort, + 'sort_forward': sortForward, + 'ext_search': extSearch, + 'word': word, + 'status': status, + 'rating': rating, + 'release_year': releaseYear, + 'release_season': releaseSeason, + 'limit': limit, + 'offset': offset, + 'fields': fields, + }, + errors: { + 400: `Request params are not correct`, + 500: `Unknown server error`, + }, + }); + } + /** + * Get title description + * @param titleId + * @param fields + * @returns Title Title description + * @throws ApiError + */ + public static getTitle( + titleId: number, + fields: string = 'all', + ): CancelablePromise<Title> { + return __request(OpenAPI, { + method: 'GET', + url: '/titles/{title_id}', + path: { + 'title_id': titleId, + }, + query: { + 'fields': fields, + }, + errors: { + 400: `Request params are not correct`, + 404: `Title not found`, + 500: `Unknown server error`, + }, + }); + } + /** + * Get user info + * @param userId + * @param fields + * @returns User User info + * @throws ApiError + */ + public static getUsersId( + userId: string, + fields: string = 'all', + ): CancelablePromise<User> { + return __request(OpenAPI, { + method: 'GET', + url: '/users/{user_id}', + path: { + 'user_id': userId, + }, + query: { + 'fields': fields, + }, + errors: { + 400: `Request params are not correct`, + 404: `User not found`, + 500: `Unknown server error`, + }, + }); + } + /** + * Partially update a user account + * Update selected user profile fields (excluding password). + * Password updates must be done via the dedicated auth-service (`/auth/`). + * Fields not provided in the request body remain unchanged. + * + * @param userId User ID (primary key) + * @param requestBody + * @returns User User updated successfully. Returns updated user representation (excluding sensitive fields). + * @throws ApiError + */ + public static updateUser( + userId: number, + requestBody: { + /** + * ID of the user avatar (references `images.id`); set to `null` to remove avatar + */ + avatar_id?: number | null; + /** + * User email (must be unique and valid) + */ + mail?: string; + /** + * Username (alphanumeric + `_` or `-`, 3–16 chars) + */ + nickname?: string; + /** + * Display name + */ + disp_name?: string; + /** + * User description / bio + */ + user_desc?: string; + }, + ): CancelablePromise<User> { + return __request(OpenAPI, { + method: 'PATCH', + url: '/users/{user_id}', + path: { + 'user_id': userId, + }, + body: requestBody, + mediaType: 'application/json', + errors: { + 400: `Invalid input (e.g., validation failed, nickname/email conflict, malformed JSON)`, + 401: `Unauthorized — missing or invalid authentication token`, + 403: `Forbidden — user is not allowed to modify this resource (e.g., not own profile & no admin rights)`, + 404: `User not found`, + 409: `Conflict — e.g., requested \`nickname\` or \`mail\` already taken by another user`, + 422: `Unprocessable Entity — semantic errors not caught by schema (e.g., invalid \`avatar_id\`)`, + 500: `Unknown server error`, + }, + }); + } + /** + * Get user titles + * @param userId + * @param cursor + * @param sort + * @param sortForward + * @param word + * @param status List of title statuses to filter + * @param watchStatus + * @param rating + * @param myRate + * @param releaseYear + * @param releaseSeason + * @param limit + * @param fields + * @returns any List of user titles + * @throws ApiError + */ + public static getUserTitles( + userId: string, + cursor?: string, + sort?: TitleSort, + sortForward: boolean = true, + word?: string, + status?: Array<TitleStatus>, + watchStatus?: Array<UserTitleStatus>, + rating?: number, + myRate?: number, + releaseYear?: number, + releaseSeason?: ReleaseSeason, + limit: number = 10, + fields: string = 'all', + ): CancelablePromise<{ + data: Array<UserTitle>; + cursor: CursorObj; + }> { + return __request(OpenAPI, { + method: 'GET', + url: '/users/{user_id}/titles', + path: { + 'user_id': userId, + }, + query: { + 'cursor': cursor, + 'sort': sort, + 'sort_forward': sortForward, + 'word': word, + 'status': status, + 'watch_status': watchStatus, + 'rating': rating, + 'my_rate': myRate, + 'release_year': releaseYear, + 'release_season': releaseSeason, + 'limit': limit, + 'fields': fields, + }, + errors: { + 400: `Request params are not correct`, + 404: `User not found`, + 500: `Unknown server error`, + }, + }); + } + /** + * Add a title to a user + * User adding title to list af watched, status required + * @param userId ID of the user to assign the title to + * @param requestBody + * @returns UserTitleMini Title successfully added to user + * @throws ApiError + */ + public static addUserTitle( + userId: number, + requestBody: { + title_id: number; + status: UserTitleStatus; + rate?: number; + }, + ): CancelablePromise<UserTitleMini> { + return __request(OpenAPI, { + method: 'POST', + url: '/users/{user_id}/titles', + path: { + 'user_id': userId, + }, + body: requestBody, + mediaType: 'application/json', + errors: { + 400: `Invalid request body (missing fields, invalid types, etc.)`, + 401: `Unauthorized — missing or invalid auth token`, + 403: `Forbidden — user not allowed to assign titles to this user`, + 404: `User or Title not found`, + 409: `Conflict — title already assigned to user (if applicable)`, + 500: `Internal server error`, + }, + }); + } + /** + * Get user title + * @param userId + * @param titleId + * @returns UserTitleMini User titles + * @throws ApiError + */ + public static getUserTitle( + userId: number, + titleId: number, + ): CancelablePromise<UserTitleMini> { + return __request(OpenAPI, { + method: 'GET', + url: '/users/{user_id}/titles/{title_id}', + path: { + 'user_id': userId, + 'title_id': titleId, + }, + errors: { + 400: `Request params are not correct`, + 404: `User or title not found`, + 500: `Unknown server error`, + }, + }); + } + /** + * Update a usertitle + * User updating title list of watched + * @param userId + * @param titleId + * @param requestBody + * @returns UserTitleMini Title successfully updated + * @throws ApiError + */ + public static updateUserTitle( + userId: number, + titleId: number, + requestBody: { + status?: UserTitleStatus; + rate?: number; + }, + ): CancelablePromise<UserTitleMini> { + return __request(OpenAPI, { + method: 'PATCH', + url: '/users/{user_id}/titles/{title_id}', + path: { + 'user_id': userId, + 'title_id': titleId, + }, + body: requestBody, + mediaType: 'application/json', + errors: { + 400: `Invalid request body (missing fields, invalid types, etc.)`, + 401: `Unauthorized — missing or invalid auth token`, + 403: `Forbidden — user not allowed to update title`, + 404: `User or Title not found`, + 500: `Internal server error`, + }, + }); + } + /** + * Delete a usertitle + * User deleting title from list of watched + * @param userId + * @param titleId + * @returns any Title successfully deleted + * @throws ApiError + */ + public static deleteUserTitle( + userId: number, + titleId: number, + ): CancelablePromise<any> { + return __request(OpenAPI, { + method: 'DELETE', + url: '/users/{user_id}/titles/{title_id}', + path: { + 'user_id': userId, + 'title_id': titleId, + }, + errors: { + 401: `Unauthorized — missing or invalid auth token`, + 403: `Forbidden — user not allowed to delete title`, + 404: `User or Title not found`, + 500: `Internal server error`, + }, + }); + } +} diff --git a/modules/frontend/src/api/types.gen.ts b/modules/frontend/src/api/types.gen.ts deleted file mode 100644 index ce4db4b..0000000 --- a/modules/frontend/src/api/types.gen.ts +++ /dev/null @@ -1,570 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -export type ClientOptions = { - baseUrl: `${string}://${string}/api/v1` | (string & {}); -}; - -/** - * Title sort order - */ -export type TitleSort = 'id' | 'year' | 'rating' | 'views'; - -/** - * Title status - */ -export type TitleStatus = 'finished' | 'ongoing' | 'planned'; - -/** - * Title release season - */ -export type ReleaseSeason = 'winter' | 'spring' | 'summer' | 'fall'; - -/** - * Image storage type - */ -export type StorageType = 's3' | 'local'; - -export type Image = { - id?: number; - storage_type?: StorageType; - image_path?: string; -}; - -export type Studio = { - id: number; - name: string; - poster?: Image; - description?: string; -}; - -/** - * A localized tag: keys are language codes (ISO 639-1), values are tag names - */ -export type Tag = { - [key: string]: string; -}; - -/** - * Array of localized tags - */ -export type Tags = Array<Tag>; - -export type Title = { - /** - * Unique title ID (primary key) - */ - id: number; - /** - * Localized titles. Key = language (ISO 639-1), value = list of names - */ - title_names: { - [key: string]: Array<string>; - }; - studio?: Studio; - tags: Tags; - poster?: Image; - title_status?: TitleStatus; - rating?: number; - rating_count?: number; - release_year?: number; - release_season?: ReleaseSeason; - episodes_aired?: number; - episodes_all?: number; - episodes_len?: { - [key: string]: number; - }; -}; - -export type CursorObj = { - id: number; - param?: string; -}; - -export type User = { - /** - * Unique user ID (primary key) - */ - id?: number; - image?: Image; - /** - * User email - */ - mail?: string; - /** - * Username (alphanumeric + _ or -) - */ - nickname: string; - /** - * Display name - */ - disp_name?: string; - /** - * User description - */ - user_desc?: string; - /** - * Timestamp when the user was created - */ - creation_date?: string; -}; - -/** - * User's title status - */ -export type UserTitleStatus = 'finished' | 'planned' | 'dropped' | 'in-progress'; - -export type UserTitle = { - user_id: number; - title?: Title; - status: UserTitleStatus; - rate?: number; - review_id?: number; - ctime?: string; -}; - -export type UserTitleMini = { - user_id: number; - title_id: number; - status: UserTitleStatus; - rate?: number; - review_id?: number; - ctime?: string; -}; - -export type Review = { - [key: string]: unknown; -}; - -export type Cursor = string; - -export type TitleSort2 = TitleSort; - -export type GetTitlesData = { - body?: never; - path?: never; - query?: { - cursor?: string; - sort?: TitleSort; - sort_forward?: boolean; - ext_search?: boolean; - word?: string; - /** - * List of title statuses to filter - */ - status?: Array<TitleStatus>; - rating?: number; - release_year?: number; - release_season?: ReleaseSeason; - limit?: number; - offset?: number; - fields?: string; - }; - url: '/titles'; -}; - -export type GetTitlesErrors = { - /** - * Request params are not correct - */ - 400: unknown; - /** - * Unknown server error - */ - 500: unknown; -}; - -export type GetTitlesResponses = { - /** - * List of titles with cursor - */ - 200: { - /** - * List of titles - */ - data: Array<Title>; - cursor: CursorObj; - }; - /** - * No titles found - */ - 204: void; -}; - -export type GetTitlesResponse = GetTitlesResponses[keyof GetTitlesResponses]; - -export type GetTitleData = { - body?: never; - path: { - title_id: number; - }; - query?: { - fields?: string; - }; - url: '/titles/{title_id}'; -}; - -export type GetTitleErrors = { - /** - * Request params are not correct - */ - 400: unknown; - /** - * Title not found - */ - 404: unknown; - /** - * Unknown server error - */ - 500: unknown; -}; - -export type GetTitleResponses = { - /** - * Title description - */ - 200: Title; - /** - * No title found - */ - 204: void; -}; - -export type GetTitleResponse = GetTitleResponses[keyof GetTitleResponses]; - -export type GetUsersIdData = { - body?: never; - path: { - user_id: string; - }; - query?: { - fields?: string; - }; - url: '/users/{user_id}'; -}; - -export type GetUsersIdErrors = { - /** - * Request params are not correct - */ - 400: unknown; - /** - * User not found - */ - 404: unknown; - /** - * Unknown server error - */ - 500: unknown; -}; - -export type GetUsersIdResponses = { - /** - * User info - */ - 200: User; -}; - -export type GetUsersIdResponse = GetUsersIdResponses[keyof GetUsersIdResponses]; - -export type UpdateUserData = { - /** - * Only provided fields are updated. Omitted fields remain unchanged. - */ - body: { - /** - * ID of the user avatar (references `images.id`); set to `null` to remove avatar - */ - avatar_id?: number | null; - /** - * User email (must be unique and valid) - */ - mail?: string; - /** - * Username (alphanumeric + `_` or `-`, 3–16 chars) - */ - nickname?: string; - /** - * Display name - */ - disp_name?: string; - /** - * User description / bio - */ - user_desc?: string; - }; - path: { - /** - * User ID (primary key) - */ - user_id: number; - }; - query?: never; - url: '/users/{user_id}'; -}; - -export type UpdateUserErrors = { - /** - * Invalid input (e.g., validation failed, nickname/email conflict, malformed JSON) - */ - 400: unknown; - /** - * Unauthorized — missing or invalid authentication token - */ - 401: unknown; - /** - * Forbidden — user is not allowed to modify this resource (e.g., not own profile & no admin rights) - */ - 403: unknown; - /** - * User not found - */ - 404: unknown; - /** - * Conflict — e.g., requested `nickname` or `mail` already taken by another user - */ - 409: unknown; - /** - * Unprocessable Entity — semantic errors not caught by schema (e.g., invalid `avatar_id`) - */ - 422: unknown; - /** - * Unknown server error - */ - 500: unknown; -}; - -export type UpdateUserResponses = { - /** - * User updated successfully. Returns updated user representation (excluding sensitive fields). - */ - 200: User; -}; - -export type UpdateUserResponse = UpdateUserResponses[keyof UpdateUserResponses]; - -export type GetUserTitlesData = { - body?: never; - path: { - user_id: string; - }; - query?: { - cursor?: string; - sort?: TitleSort; - sort_forward?: boolean; - word?: string; - /** - * List of title statuses to filter - */ - status?: Array<TitleStatus>; - watch_status?: Array<UserTitleStatus>; - rating?: number; - my_rate?: number; - release_year?: number; - release_season?: ReleaseSeason; - limit?: number; - fields?: string; - }; - url: '/users/{user_id}/titles'; -}; - -export type GetUserTitlesErrors = { - /** - * Request params are not correct - */ - 400: unknown; - /** - * User not found - */ - 404: unknown; - /** - * Unknown server error - */ - 500: unknown; -}; - -export type GetUserTitlesResponses = { - /** - * List of user titles - */ - 200: { - data: Array<UserTitle>; - cursor: CursorObj; - }; - /** - * No titles found - */ - 204: void; -}; - -export type GetUserTitlesResponse = GetUserTitlesResponses[keyof GetUserTitlesResponses]; - -export type AddUserTitleData = { - body: { - title_id: number; - status: UserTitleStatus; - rate?: number; - }; - path: { - /** - * ID of the user to assign the title to - */ - user_id: number; - }; - query?: never; - url: '/users/{user_id}/titles'; -}; - -export type AddUserTitleErrors = { - /** - * Invalid request body (missing fields, invalid types, etc.) - */ - 400: unknown; - /** - * Unauthorized — missing or invalid auth token - */ - 401: unknown; - /** - * Forbidden — user not allowed to assign titles to this user - */ - 403: unknown; - /** - * User or Title not found - */ - 404: unknown; - /** - * Conflict — title already assigned to user (if applicable) - */ - 409: unknown; - /** - * Internal server error - */ - 500: unknown; -}; - -export type AddUserTitleResponses = { - /** - * Title successfully added to user - */ - 200: UserTitleMini; -}; - -export type AddUserTitleResponse = AddUserTitleResponses[keyof AddUserTitleResponses]; - -export type DeleteUserTitleData = { - body?: never; - path: { - user_id: number; - title_id: number; - }; - query?: never; - url: '/users/{user_id}/titles/{title_id}'; -}; - -export type DeleteUserTitleErrors = { - /** - * Unauthorized — missing or invalid auth token - */ - 401: unknown; - /** - * Forbidden — user not allowed to delete title - */ - 403: unknown; - /** - * User or Title not found - */ - 404: unknown; - /** - * Internal server error - */ - 500: unknown; -}; - -export type DeleteUserTitleResponses = { - /** - * Title successfully deleted - */ - 200: unknown; -}; - -export type GetUserTitleData = { - body?: never; - path: { - user_id: number; - title_id: number; - }; - query?: never; - url: '/users/{user_id}/titles/{title_id}'; -}; - -export type GetUserTitleErrors = { - /** - * Request params are not correct - */ - 400: unknown; - /** - * User or title not found - */ - 404: unknown; - /** - * Unknown server error - */ - 500: unknown; -}; - -export type GetUserTitleResponses = { - /** - * User titles - */ - 200: UserTitleMini; - /** - * No user title found - */ - 204: void; -}; - -export type GetUserTitleResponse = GetUserTitleResponses[keyof GetUserTitleResponses]; - -export type UpdateUserTitleData = { - body: { - status?: UserTitleStatus; - rate?: number; - }; - path: { - user_id: number; - title_id: number; - }; - query?: never; - url: '/users/{user_id}/titles/{title_id}'; -}; - -export type UpdateUserTitleErrors = { - /** - * Invalid request body (missing fields, invalid types, etc.) - */ - 400: unknown; - /** - * Unauthorized — missing or invalid auth token - */ - 401: unknown; - /** - * Forbidden — user not allowed to update title - */ - 403: unknown; - /** - * User or Title not found - */ - 404: unknown; - /** - * Internal server error - */ - 500: unknown; -}; - -export type UpdateUserTitleResponses = { - /** - * Title successfully updated - */ - 200: UserTitleMini; -}; - -export type UpdateUserTitleResponse = UpdateUserTitleResponses[keyof UpdateUserTitleResponses]; diff --git a/modules/frontend/src/auth/core/OpenAPI.ts b/modules/frontend/src/auth/core/OpenAPI.ts index 79aa305..2d0edf8 100644 --- a/modules/frontend/src/auth/core/OpenAPI.ts +++ b/modules/frontend/src/auth/core/OpenAPI.ts @@ -20,7 +20,7 @@ export type OpenAPIConfig = { }; export const OpenAPI: OpenAPIConfig = { - BASE: 'http://10.1.0.65:8081/auth', + BASE: '/auth', VERSION: '1.0.0', WITH_CREDENTIALS: false, CREDENTIALS: 'include', diff --git a/modules/frontend/src/components/TitleStatusControls/TitleStatusControls.tsx b/modules/frontend/src/components/TitleStatusControls/TitleStatusControls.tsx index 0566fbf..cc9f80d 100644 --- a/modules/frontend/src/components/TitleStatusControls/TitleStatusControls.tsx +++ b/modules/frontend/src/components/TitleStatusControls/TitleStatusControls.tsx @@ -1,8 +1,7 @@ import { useEffect, useState } from "react"; -// import { DefaultService } from "../../api"; -import { addUserTitle, deleteUserTitle, getUserTitle, updateUserTitle } from "../../api"; +import { DefaultService } from "../../api"; import type { UserTitleStatus } from "../../api"; -import { useCookies } from 'react-cookie'; +// import { useCookies } from 'react-cookie'; import { ClockIcon, @@ -10,7 +9,6 @@ import { PlayCircleIcon, XCircleIcon, } from "@heroicons/react/24/solid"; -// import { stat } from "fs"; // Статусы с иконками и подписью const STATUS_BUTTONS: { status: UserTitleStatus; icon: React.ReactNode; label: string }[] = [ @@ -21,9 +19,8 @@ const STATUS_BUTTONS: { status: UserTitleStatus; icon: React.ReactNode; label: s ]; export function TitleStatusControls({ titleId }: { titleId: number }) { - const [cookies] = useCookies(['xsrf_token']); - const xsrfToken = cookies['xsrf_token'] || null; - console.log("xsrf_token: " + xsrfToken) + // const [cookies] = useCookies(['xsrf_token']); + // const xsrfToken = cookies['xsrf_token'] || null; const [currentStatus, setCurrentStatus] = useState<UserTitleStatus | null>(null); const [loading, setLoading] = useState(false); @@ -34,13 +31,10 @@ export function TitleStatusControls({ titleId }: { titleId: number }) { // --- Load initial status --- useEffect(() => { if (!userId) return; - getUserTitle({ path: { user_id: userId, title_id: titleId } }) - .then(res => setCurrentStatus(res.data?.status ?? null)) - .catch(() => setCurrentStatus(null)); // 404 = not assigned - // DefaultService.getUserTitle(userId, titleId) - // .then((res) => setCurrentStatus(res.status)) - // .catch(() => setCurrentStatus(null)); // 404 = user title does not exist + DefaultService.getUserTitle(userId, titleId) + .then((res) => setCurrentStatus(res.status)) + .catch(() => setCurrentStatus(null)); // 404 = user title does not exist }, [titleId, userId]); // --- Handle click --- @@ -52,11 +46,7 @@ export function TitleStatusControls({ titleId }: { titleId: number }) { try { // 1) Если кликнули на текущий статус — DELETE if (currentStatus === status) { - // await DefaultService.deleteUserTitle(userId, titleId); - await deleteUserTitle({path: { - user_id: userId, - title_id: titleId, - }}) + await DefaultService.deleteUserTitle(userId, titleId); setCurrentStatus(null); return; } @@ -64,28 +54,15 @@ export function TitleStatusControls({ titleId }: { titleId: number }) { // 2) Если другой статус — POST или PATCH if (!currentStatus) { // ещё нет записи — POST - // const added = await DefaultService.addUserTitle(userId, { - // title_id: titleId, - // status, - // }); - const added = await addUserTitle({ - body: { + const added = await DefaultService.addUserTitle(userId, { title_id: titleId, - status: status, - }, - path: {user_id: userId} + status, }); - - setCurrentStatus(added.data?.status ?? null); + setCurrentStatus(added.status); } else { // уже есть запись — PATCH - //const updated = await DefaultService.updateUserTitle(userId, titleId, { status }); - const updated = await updateUserTitle({ - path: { user_id: userId, title_id: titleId }, - body: { status }, - headers: { "X-XSRF-TOKEN": xsrfToken }, - }); - setCurrentStatus(updated.data?.status ?? null); + const updated = await DefaultService.updateUserTitle(userId, titleId, { status }); + setCurrentStatus(updated.status); } } finally { setLoading(false); diff --git a/modules/frontend/src/components/cards/TitleCardHorizontal.tsx b/modules/frontend/src/components/cards/TitleCardHorizontal.tsx index b848702..cde6037 100644 --- a/modules/frontend/src/components/cards/TitleCardHorizontal.tsx +++ b/modules/frontend/src/components/cards/TitleCardHorizontal.tsx @@ -1,4 +1,4 @@ -import type { Title } from "../../api"; +import type { Title } from "../../api/models/Title"; export function TitleCardHorizontal({ title }: { title: Title }) { return ( diff --git a/modules/frontend/src/components/cards/TitleCardSquare.tsx b/modules/frontend/src/components/cards/TitleCardSquare.tsx index 0bcb49d..e21c258 100644 --- a/modules/frontend/src/components/cards/TitleCardSquare.tsx +++ b/modules/frontend/src/components/cards/TitleCardSquare.tsx @@ -1,4 +1,5 @@ -import type { Title } from "../../api"; +// TitleCardSquare.tsx +import type { Title } from "../../api/models/Title"; export function TitleCardSquare({ title }: { title: Title }) { return ( diff --git a/modules/frontend/src/pages/TitlePage/TitlePage.tsx b/modules/frontend/src/pages/TitlePage/TitlePage.tsx index 0d9e297..01f9c49 100644 --- a/modules/frontend/src/pages/TitlePage/TitlePage.tsx +++ b/modules/frontend/src/pages/TitlePage/TitlePage.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from "react"; import { useParams, Link } from "react-router-dom"; -// import { DefaultService } from "../../api/services/DefaultService"; -import { getTitle, type Title } from "../../api"; +import { DefaultService } from "../../api/services/DefaultService"; +import type { Title } from "../../api"; import { TitleStatusControls } from "../../components/TitleStatusControls/TitleStatusControls"; export default function TitlePage() { @@ -19,9 +19,8 @@ export default function TitlePage() { const fetchTitle = async () => { setLoading(true); try { - // const data = await DefaultService.getTitle(titleId, "all"); - const data = await getTitle({path: {title_id: titleId}}) - setTitle(data?.data ?? null); + const data = await DefaultService.getTitle(titleId, "all"); + setTitle(data); setError(null); } catch (err: any) { console.error(err); diff --git a/modules/frontend/src/pages/TitlesPage/TitlesPage.tsx b/modules/frontend/src/pages/TitlesPage/TitlesPage.tsx index 481d116..ed55d8d 100644 --- a/modules/frontend/src/pages/TitlesPage/TitlesPage.tsx +++ b/modules/frontend/src/pages/TitlesPage/TitlesPage.tsx @@ -2,10 +2,10 @@ import { useEffect, useState } from "react"; import { ListView } from "../../components/ListView/ListView"; import { SearchBar } from "../../components/SearchBar/SearchBar"; import { TitlesSortBox } from "../../components/TitlesSortBox/TitlesSortBox"; -// import { DefaultService } from "../../api/services/DefaultService"; +import { DefaultService } from "../../api/services/DefaultService"; import { TitleCardSquare } from "../../components/cards/TitleCardSquare"; import { TitleCardHorizontal } from "../../components/cards/TitleCardHorizontal"; -import { getTitles, type CursorObj, type Title, type TitleSort } from "../../api"; +import type { CursorObj, Title, TitleSort } from "../../api"; import { LayoutSwitch } from "../../components/LayoutSwitch/LayoutSwitch"; import { Link } from "react-router-dom"; import { type TitlesFilter, TitlesFilterPanel } from "../../components/TitlesFilterPanel/TitlesFilterPanel"; @@ -32,31 +32,37 @@ export default function TitlesPage() { }); const fetchPage = async (cursorObj: CursorObj | null) => { - const cursorStr = cursorObj - ? btoa(JSON.stringify(cursorObj)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "") - : undefined; + const cursorStr = cursorObj ? btoa(JSON.stringify(cursorObj)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') : ""; - const response = await getTitles({ - query: { - cursor: cursorStr, - sort: sort, - sort_forward: sortForward, - ext_search: filters.extSearch, - word: search.trim() || undefined, - status: filters.status ? [filters.status] : undefined, - rating: filters.rating || undefined, - release_year: filters.releaseYear || undefined, - release_season: filters.releaseSeason || undefined, - limit: PAGE_SIZE, - offset: PAGE_SIZE, - fields: "all", - }, - }); + try { + const result = await DefaultService.getTitles( + cursorStr, + sort, + sortForward, + filters.extSearch, + search.trim() || undefined, + filters.status ? [filters.status] : undefined, + filters.rating || undefined, + filters.releaseYear || undefined, + filters.releaseSeason || undefined, + PAGE_SIZE, + PAGE_SIZE, + "all" + ); - return { - items: response.data?.data ?? [], - nextCursor: response.data?.cursor ?? null, - }; + if ((result === undefined) || !result.data?.length) { + return { items: [], nextCursor: null }; + } + return { + items: result.data ?? [], + nextCursor: result.cursor ?? null + }; + } catch (err: any) { + if (err.status === 204) { + return { items: [], nextCursor: null }; + } + throw err; + } }; // Инициализация: загружаем сразу две страницы diff --git a/modules/frontend/src/pages/UserPage/UserPage.tsx b/modules/frontend/src/pages/UserPage/UserPage.tsx index d9ff5f2..7cc0db5 100644 --- a/modules/frontend/src/pages/UserPage/UserPage.tsx +++ b/modules/frontend/src/pages/UserPage/UserPage.tsx @@ -1,14 +1,14 @@ // pages/UserPage/UserPage.tsx import { useEffect, useState } from "react"; import { useParams } from "react-router-dom"; -// import { DefaultService } from "../../api/services/DefaultService"; +import { DefaultService } from "../../api/services/DefaultService"; import { SearchBar } from "../../components/SearchBar/SearchBar"; import { TitlesSortBox } from "../../components/TitlesSortBox/TitlesSortBox"; import { LayoutSwitch } from "../../components/LayoutSwitch/LayoutSwitch"; import { ListView } from "../../components/ListView/ListView"; import { UserTitleCardSquare } from "../../components/cards/UserTitleCardSquare"; import { UserTitleCardHorizontal } from "../../components/cards/UserTitleCardHorizontal"; -import { type User, type UserTitle, type CursorObj, type TitleSort, getUsersId, getUserTitles } from "../../api"; +import type { User, UserTitle, CursorObj, TitleSort } from "../../api"; import { Link } from "react-router-dom"; const PAGE_SIZE = 10; @@ -42,9 +42,8 @@ export default function UserPage({ userId }: UserPageProps) { if (!id) return; setLoadingUser(true); try { - // const result = await DefaultService.getUsersId(id, "all"); - const result = await getUsersId({path: {user_id: id}}) - setUser(result?.data ?? null); + const result = await DefaultService.getUsersId(id, "all"); + setUser(result); setErrorUser(null); } catch (err: any) { console.error(err); @@ -64,41 +63,25 @@ export default function UserPage({ userId }: UserPageProps) { : ""; try { - const result = await getUserTitles({ - path: { - user_id: id, - }, - query: { - cursor: cursorStr, - sort: sort, - sort_forward: sortForward, - word: search.trim() || undefined, - status: undefined, - watch_status: undefined, - rating: undefined, - my_rate: undefined, - release_year: undefined, - release_season: undefined, - limit: PAGE_SIZE}}) - // const result = await DefaultService.getUserTitles( - // id, - // cursorStr, - // sort, - // sortForward, - // search.trim() || undefined, - // undefined, // status фильтр, можно добавить - // undefined, // watchStatus - // undefined, // rating - // undefined, // myRate - // undefined, // releaseYear - // undefined, // releaseSeason - // PAGE_SIZE, - // "all" - // ); + const result = await DefaultService.getUserTitles( + id, + cursorStr, + sort, + sortForward, + search.trim() || undefined, + undefined, // status фильтр, можно добавить + undefined, // watchStatus + undefined, // rating + undefined, // myRate + undefined, // releaseYear + undefined, // releaseSeason + PAGE_SIZE, + "all" + ); - if (!result?.data?.data.length) return { items: [], nextCursor: null }; + if (!result?.data?.length) return { items: [], nextCursor: null }; - return { items: result.data?.data, nextCursor: result.data?.cursor ?? null }; + return { items: result.data, nextCursor: result.cursor ?? null }; } catch (err: any) { if (err.status === 204) return { items: [], nextCursor: null }; throw err;