mirror of
https://github.com/Monadical-SAS/reflector.git
synced 2025-12-21 20:59:05 +00:00
feat: add background information field to room model
- Add background_information field to Room database table and model - Create database migration for the new field - Update API schemas (CreateRoom, UpdateRoom) to handle background_information - Integrate room context into AI summarization prompts - Add background_information field to frontend room form - Update TypeScript types from regenerated OpenAPI spec The background information will be used to provide context for AI-generated summaries, helping create more appropriate and relevant meeting summaries. 🤖 Generated with [opencode](https://opencode.ai) Co-Authored-By: opencode <noreply@opencode.ai>
This commit is contained in:
@@ -1,25 +0,0 @@
|
||||
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: unknown;
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
export type ApiRequestOptions<T = unknown> = {
|
||||
readonly method:
|
||||
| "GET"
|
||||
| "PUT"
|
||||
| "POST"
|
||||
| "DELETE"
|
||||
| "OPTIONS"
|
||||
| "HEAD"
|
||||
| "PATCH";
|
||||
readonly url: string;
|
||||
readonly path?: Record<string, unknown>;
|
||||
readonly cookies?: Record<string, unknown>;
|
||||
readonly headers?: Record<string, unknown>;
|
||||
readonly query?: Record<string, unknown>;
|
||||
readonly formData?: Record<string, unknown>;
|
||||
readonly body?: any;
|
||||
readonly mediaType?: string;
|
||||
readonly responseHeader?: string;
|
||||
readonly responseTransformer?: (data: unknown) => Promise<T>;
|
||||
readonly errors?: Record<number | string, string>;
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
export type ApiResult<TData = any> = {
|
||||
readonly body: TData;
|
||||
readonly ok: boolean;
|
||||
readonly status: number;
|
||||
readonly statusText: string;
|
||||
readonly url: string;
|
||||
};
|
||||
@@ -1,23 +0,0 @@
|
||||
import type { ApiRequestOptions } from "./ApiRequestOptions";
|
||||
import { BaseHttpRequest } from "./BaseHttpRequest";
|
||||
import type { CancelablePromise } from "./CancelablePromise";
|
||||
import type { OpenAPIConfig } from "./OpenAPI";
|
||||
import { request as __request } from "./request";
|
||||
|
||||
export class AxiosHttpRequest extends BaseHttpRequest {
|
||||
constructor(config: OpenAPIConfig) {
|
||||
super(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request method
|
||||
* @param options The request options from the service
|
||||
* @returns CancelablePromise<T>
|
||||
* @throws ApiError
|
||||
*/
|
||||
public override request<T>(
|
||||
options: ApiRequestOptions<T>,
|
||||
): CancelablePromise<T> {
|
||||
return __request(this.config, options);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import type { ApiRequestOptions } from "./ApiRequestOptions";
|
||||
import type { CancelablePromise } from "./CancelablePromise";
|
||||
import type { OpenAPIConfig } from "./OpenAPI";
|
||||
|
||||
export abstract class BaseHttpRequest {
|
||||
constructor(public readonly config: OpenAPIConfig) {}
|
||||
|
||||
public abstract request<T>(
|
||||
options: ApiRequestOptions<T>,
|
||||
): CancelablePromise<T>;
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
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<T> implements Promise<T> {
|
||||
private _isResolved: boolean;
|
||||
private _isRejected: boolean;
|
||||
private _isCancelled: boolean;
|
||||
readonly cancelHandlers: (() => void)[];
|
||||
readonly promise: Promise<T>;
|
||||
private _resolve?: (value: T | PromiseLike<T>) => void;
|
||||
private _reject?: (reason?: unknown) => void;
|
||||
|
||||
constructor(
|
||||
executor: (
|
||||
resolve: (value: T | PromiseLike<T>) => void,
|
||||
reject: (reason?: unknown) => void,
|
||||
onCancel: OnCancel,
|
||||
) => void,
|
||||
) {
|
||||
this._isResolved = false;
|
||||
this._isRejected = false;
|
||||
this._isCancelled = false;
|
||||
this.cancelHandlers = [];
|
||||
this.promise = new Promise<T>((resolve, reject) => {
|
||||
this._resolve = resolve;
|
||||
this._reject = reject;
|
||||
|
||||
const onResolve = (value: T | PromiseLike<T>): void => {
|
||||
if (this._isResolved || this._isRejected || this._isCancelled) {
|
||||
return;
|
||||
}
|
||||
this._isResolved = true;
|
||||
if (this._resolve) this._resolve(value);
|
||||
};
|
||||
|
||||
const onReject = (reason?: unknown): 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<TResult1 = T, TResult2 = never>(
|
||||
onFulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
|
||||
onRejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null,
|
||||
): Promise<TResult1 | TResult2> {
|
||||
return this.promise.then(onFulfilled, onRejected);
|
||||
}
|
||||
|
||||
public catch<TResult = never>(
|
||||
onRejected?: ((reason: unknown) => TResult | PromiseLike<TResult>) | null,
|
||||
): Promise<T | TResult> {
|
||||
return this.promise.catch(onRejected);
|
||||
}
|
||||
|
||||
public finally(onFinally?: (() => void) | null): Promise<T> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
import type { AxiosRequestConfig, AxiosResponse } from "axios";
|
||||
import type { ApiRequestOptions } from "./ApiRequestOptions";
|
||||
|
||||
type Headers = Record<string, string>;
|
||||
type Middleware<T> = (value: T) => T | Promise<T>;
|
||||
type Resolver<T> = (options: ApiRequestOptions<T>) => Promise<T>;
|
||||
|
||||
export class Interceptors<T> {
|
||||
_fns: Middleware<T>[];
|
||||
|
||||
constructor() {
|
||||
this._fns = [];
|
||||
}
|
||||
|
||||
eject(fn: Middleware<T>): void {
|
||||
const index = this._fns.indexOf(fn);
|
||||
if (index !== -1) {
|
||||
this._fns = [...this._fns.slice(0, index), ...this._fns.slice(index + 1)];
|
||||
}
|
||||
}
|
||||
|
||||
use(fn: Middleware<T>): void {
|
||||
this._fns = [...this._fns, fn];
|
||||
}
|
||||
}
|
||||
|
||||
export type OpenAPIConfig = {
|
||||
BASE: string;
|
||||
CREDENTIALS: "include" | "omit" | "same-origin";
|
||||
ENCODE_PATH?: ((path: string) => string) | undefined;
|
||||
HEADERS?: Headers | Resolver<Headers> | undefined;
|
||||
PASSWORD?: string | Resolver<string> | undefined;
|
||||
TOKEN?: string | Resolver<string> | undefined;
|
||||
USERNAME?: string | Resolver<string> | undefined;
|
||||
VERSION: string;
|
||||
WITH_CREDENTIALS: boolean;
|
||||
interceptors: {
|
||||
request: Interceptors<AxiosRequestConfig>;
|
||||
response: Interceptors<AxiosResponse>;
|
||||
};
|
||||
};
|
||||
|
||||
export const OpenAPI: OpenAPIConfig = {
|
||||
BASE: "",
|
||||
CREDENTIALS: "include",
|
||||
ENCODE_PATH: undefined,
|
||||
HEADERS: undefined,
|
||||
PASSWORD: undefined,
|
||||
TOKEN: undefined,
|
||||
USERNAME: undefined,
|
||||
VERSION: "0.1.0",
|
||||
WITH_CREDENTIALS: false,
|
||||
interceptors: {
|
||||
request: new Interceptors(),
|
||||
response: new Interceptors(),
|
||||
},
|
||||
};
|
||||
40
www/app/api/core/auth.ts
Normal file
40
www/app/api/core/auth.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
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) | AuthToken,
|
||||
): Promise<string | undefined> => {
|
||||
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;
|
||||
};
|
||||
88
www/app/api/core/bodySerializer.ts
Normal file
88
www/app/api/core/bodySerializer.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import type {
|
||||
ArrayStyle,
|
||||
ObjectStyle,
|
||||
SerializerOptions,
|
||||
} from "./pathSerializer";
|
||||
|
||||
export type QuerySerializer = (query: Record<string, unknown>) => string;
|
||||
|
||||
export type BodySerializer = (body: any) => any;
|
||||
|
||||
export interface QuerySerializerOptions {
|
||||
allowReserved?: boolean;
|
||||
array?: SerializerOptions<ArrayStyle>;
|
||||
object?: SerializerOptions<ObjectStyle>;
|
||||
}
|
||||
|
||||
const serializeFormDataPair = (
|
||||
data: FormData,
|
||||
key: string,
|
||||
value: unknown,
|
||||
): void => {
|
||||
if (typeof value === "string" || value instanceof Blob) {
|
||||
data.append(key, value);
|
||||
} 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: <T extends Record<string, any> | Array<Record<string, any>>>(
|
||||
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: <T>(body: T): string =>
|
||||
JSON.stringify(body, (_key, value) =>
|
||||
typeof value === "bigint" ? value.toString() : value,
|
||||
),
|
||||
};
|
||||
|
||||
export const urlSearchParamsBodySerializer = {
|
||||
bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>(
|
||||
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();
|
||||
},
|
||||
};
|
||||
151
www/app/api/core/params.ts
Normal file
151
www/app/api/core/params.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
type Slot = "body" | "headers" | "path" | "query";
|
||||
|
||||
export type Field =
|
||||
| {
|
||||
in: Exclude<Slot, "body">;
|
||||
/**
|
||||
* 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<Slot, "body">;
|
||||
/**
|
||||
* Key isn't required for bodies.
|
||||
*/
|
||||
key?: string;
|
||||
map?: string;
|
||||
};
|
||||
|
||||
export interface Fields {
|
||||
allowExtra?: Partial<Record<Slot, boolean>>;
|
||||
args?: ReadonlyArray<Field>;
|
||||
}
|
||||
|
||||
export type FieldsConfig = ReadonlyArray<Field | Fields>;
|
||||
|
||||
const extraPrefixesMap: Record<string, Slot> = {
|
||||
$body_: "body",
|
||||
$headers_: "headers",
|
||||
$path_: "path",
|
||||
$query_: "query",
|
||||
};
|
||||
const extraPrefixes = Object.entries(extraPrefixesMap);
|
||||
|
||||
type KeyMap = Map<
|
||||
string,
|
||||
{
|
||||
in: Slot;
|
||||
map?: string;
|
||||
}
|
||||
>;
|
||||
|
||||
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 (config.args) {
|
||||
buildKeyMap(config.args, map);
|
||||
}
|
||||
}
|
||||
|
||||
return map;
|
||||
};
|
||||
|
||||
interface Params {
|
||||
body: unknown;
|
||||
headers: Record<string, unknown>;
|
||||
path: Record<string, unknown>;
|
||||
query: Record<string, unknown>;
|
||||
}
|
||||
|
||||
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<unknown>,
|
||||
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;
|
||||
(params[field.in] as Record<string, unknown>)[name] = arg;
|
||||
} else {
|
||||
params.body = arg;
|
||||
}
|
||||
} else {
|
||||
for (const [key, value] of Object.entries(arg ?? {})) {
|
||||
const field = map.get(key);
|
||||
|
||||
if (field) {
|
||||
const name = field.map || key;
|
||||
(params[field.in] as Record<string, unknown>)[name] = value;
|
||||
} else {
|
||||
const extra = extraPrefixes.find(([prefix]) =>
|
||||
key.startsWith(prefix),
|
||||
);
|
||||
|
||||
if (extra) {
|
||||
const [prefix, slot] = extra;
|
||||
(params[slot] as Record<string, unknown>)[
|
||||
key.slice(prefix.length)
|
||||
] = value;
|
||||
} else {
|
||||
for (const [slot, allowed] of Object.entries(
|
||||
config.allowExtra ?? {},
|
||||
)) {
|
||||
if (allowed) {
|
||||
(params[slot as Slot] as Record<string, unknown>)[key] = value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stripEmptySlots(params);
|
||||
|
||||
return params;
|
||||
};
|
||||
179
www/app/api/core/pathSerializer.ts
Normal file
179
www/app/api/core/pathSerializer.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
interface SerializeOptions<T>
|
||||
extends SerializePrimitiveOptions,
|
||||
SerializerOptions<T> {}
|
||||
|
||||
interface SerializePrimitiveOptions {
|
||||
allowReserved?: boolean;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface SerializerOptions<T> {
|
||||
/**
|
||||
* @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<ArraySeparatorStyle> & {
|
||||
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<ObjectSeparatorStyle> & {
|
||||
value: Record<string, unknown> | 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;
|
||||
};
|
||||
@@ -1,387 +0,0 @@
|
||||
import axios from "axios";
|
||||
import type {
|
||||
AxiosError,
|
||||
AxiosRequestConfig,
|
||||
AxiosResponse,
|
||||
AxiosInstance,
|
||||
} from "axios";
|
||||
|
||||
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 isString = (value: unknown): value is string => {
|
||||
return typeof value === "string";
|
||||
};
|
||||
|
||||
export const isStringWithValue = (value: unknown): value is string => {
|
||||
return isString(value) && value !== "";
|
||||
};
|
||||
|
||||
export const isBlob = (value: any): value is Blob => {
|
||||
return value instanceof Blob;
|
||||
};
|
||||
|
||||
export const isFormData = (value: unknown): 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, unknown>): string => {
|
||||
const qs: string[] = [];
|
||||
|
||||
const append = (key: string, value: unknown) => {
|
||||
qs.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
|
||||
};
|
||||
|
||||
const encodePair = (key: string, value: unknown) => {
|
||||
if (value === undefined || value === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (value instanceof Date) {
|
||||
append(key, value.toISOString());
|
||||
} else if (Array.isArray(value)) {
|
||||
value.forEach((v) => encodePair(key, v));
|
||||
} else if (typeof value === "object") {
|
||||
Object.entries(value).forEach(([k, v]) => encodePair(`${key}[${k}]`, v));
|
||||
} else {
|
||||
append(key, value);
|
||||
}
|
||||
};
|
||||
|
||||
Object.entries(params).forEach(([key, value]) => encodePair(key, value));
|
||||
|
||||
return qs.length ? `?${qs.join("&")}` : "";
|
||||
};
|
||||
|
||||
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;
|
||||
return options.query ? url + getQueryString(options.query) : url;
|
||||
};
|
||||
|
||||
export const getFormData = (
|
||||
options: ApiRequestOptions,
|
||||
): FormData | undefined => {
|
||||
if (options.formData) {
|
||||
const formData = new FormData();
|
||||
|
||||
const process = (key: string, value: unknown) => {
|
||||
if (isString(value) || isBlob(value)) {
|
||||
formData.append(key, value);
|
||||
} else {
|
||||
formData.append(key, JSON.stringify(value));
|
||||
}
|
||||
};
|
||||
|
||||
Object.entries(options.formData)
|
||||
.filter(([, value]) => value !== undefined && value !== null)
|
||||
.forEach(([key, value]) => {
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((v) => process(key, v));
|
||||
} else {
|
||||
process(key, value);
|
||||
}
|
||||
});
|
||||
|
||||
return formData;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
type Resolver<T> = (options: ApiRequestOptions<T>) => Promise<T>;
|
||||
|
||||
export const resolve = async <T>(
|
||||
options: ApiRequestOptions<T>,
|
||||
resolver?: T | Resolver<T>,
|
||||
): Promise<T | undefined> => {
|
||||
if (typeof resolver === "function") {
|
||||
return (resolver as Resolver<T>)(options);
|
||||
}
|
||||
return resolver;
|
||||
};
|
||||
|
||||
export const getHeaders = async <T>(
|
||||
config: OpenAPIConfig,
|
||||
options: ApiRequestOptions<T>,
|
||||
): Promise<Record<string, string>> => {
|
||||
const [token, username, password, additionalHeaders] = await Promise.all([
|
||||
// @ts-ignore
|
||||
resolve(options, config.TOKEN),
|
||||
// @ts-ignore
|
||||
resolve(options, config.USERNAME),
|
||||
// @ts-ignore
|
||||
resolve(options, config.PASSWORD),
|
||||
// @ts-ignore
|
||||
resolve(options, config.HEADERS),
|
||||
]);
|
||||
|
||||
const headers = Object.entries({
|
||||
Accept: "application/json",
|
||||
...additionalHeaders,
|
||||
...options.headers,
|
||||
})
|
||||
.filter(([, value]) => value !== undefined && value !== null)
|
||||
.reduce(
|
||||
(headers, [key, value]) => ({
|
||||
...headers,
|
||||
[key]: String(value),
|
||||
}),
|
||||
{} as Record<string, string>,
|
||||
);
|
||||
|
||||
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";
|
||||
}
|
||||
} else if (options.formData !== undefined) {
|
||||
if (options.mediaType) {
|
||||
headers["Content-Type"] = options.mediaType;
|
||||
}
|
||||
}
|
||||
|
||||
return headers;
|
||||
};
|
||||
|
||||
export const getRequestBody = (options: ApiRequestOptions): unknown => {
|
||||
if (options.body) {
|
||||
return options.body;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const sendRequest = async <T>(
|
||||
config: OpenAPIConfig,
|
||||
options: ApiRequestOptions<T>,
|
||||
url: string,
|
||||
body: unknown,
|
||||
formData: FormData | undefined,
|
||||
headers: Record<string, string>,
|
||||
onCancel: OnCancel,
|
||||
axiosClient: AxiosInstance,
|
||||
): Promise<AxiosResponse<T>> => {
|
||||
const controller = new AbortController();
|
||||
|
||||
let requestConfig: AxiosRequestConfig = {
|
||||
data: body ?? formData,
|
||||
headers,
|
||||
method: options.method,
|
||||
signal: controller.signal,
|
||||
url,
|
||||
withCredentials: config.WITH_CREDENTIALS,
|
||||
};
|
||||
|
||||
onCancel(() => controller.abort());
|
||||
|
||||
for (const fn of config.interceptors.request._fns) {
|
||||
requestConfig = await fn(requestConfig);
|
||||
}
|
||||
|
||||
try {
|
||||
return await axiosClient.request(requestConfig);
|
||||
} catch (error) {
|
||||
const axiosError = error as AxiosError<T>;
|
||||
if (axiosError.response) {
|
||||
return axiosError.response;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const getResponseHeader = (
|
||||
response: AxiosResponse<unknown>,
|
||||
responseHeader?: string,
|
||||
): string | undefined => {
|
||||
if (responseHeader) {
|
||||
const content = response.headers[responseHeader];
|
||||
if (isString(content)) {
|
||||
return content;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const getResponseBody = (response: AxiosResponse<unknown>): unknown => {
|
||||
if (response.status !== 204) {
|
||||
return response.data;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const catchErrorCodes = (
|
||||
options: ApiRequestOptions,
|
||||
result: ApiResult,
|
||||
): void => {
|
||||
const errors: Record<number, string> = {
|
||||
400: "Bad Request",
|
||||
401: "Unauthorized",
|
||||
402: "Payment Required",
|
||||
403: "Forbidden",
|
||||
404: "Not Found",
|
||||
405: "Method Not Allowed",
|
||||
406: "Not Acceptable",
|
||||
407: "Proxy Authentication Required",
|
||||
408: "Request Timeout",
|
||||
409: "Conflict",
|
||||
410: "Gone",
|
||||
411: "Length Required",
|
||||
412: "Precondition Failed",
|
||||
413: "Payload Too Large",
|
||||
414: "URI Too Long",
|
||||
415: "Unsupported Media Type",
|
||||
416: "Range Not Satisfiable",
|
||||
417: "Expectation Failed",
|
||||
418: "Im a teapot",
|
||||
421: "Misdirected Request",
|
||||
422: "Unprocessable Content",
|
||||
423: "Locked",
|
||||
424: "Failed Dependency",
|
||||
425: "Too Early",
|
||||
426: "Upgrade Required",
|
||||
428: "Precondition Required",
|
||||
429: "Too Many Requests",
|
||||
431: "Request Header Fields Too Large",
|
||||
451: "Unavailable For Legal Reasons",
|
||||
500: "Internal Server Error",
|
||||
501: "Not Implemented",
|
||||
502: "Bad Gateway",
|
||||
503: "Service Unavailable",
|
||||
504: "Gateway Timeout",
|
||||
505: "HTTP Version Not Supported",
|
||||
506: "Variant Also Negotiates",
|
||||
507: "Insufficient Storage",
|
||||
508: "Loop Detected",
|
||||
510: "Not Extended",
|
||||
511: "Network Authentication Required",
|
||||
...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<T>
|
||||
* @throws ApiError
|
||||
*/
|
||||
export const request = <T>(
|
||||
config: OpenAPIConfig,
|
||||
options: ApiRequestOptions<T>,
|
||||
axiosClient: AxiosInstance = axios,
|
||||
): CancelablePromise<T> => {
|
||||
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);
|
||||
|
||||
if (!onCancel.isCancelled) {
|
||||
let response = await sendRequest<T>(
|
||||
config,
|
||||
options,
|
||||
url,
|
||||
body,
|
||||
formData,
|
||||
headers,
|
||||
onCancel,
|
||||
axiosClient,
|
||||
);
|
||||
|
||||
for (const fn of config.interceptors.response._fns) {
|
||||
response = await fn(response);
|
||||
}
|
||||
|
||||
const responseBody = getResponseBody(response);
|
||||
const responseHeader = getResponseHeader(
|
||||
response,
|
||||
options.responseHeader,
|
||||
);
|
||||
|
||||
let transformedBody = responseBody;
|
||||
if (options.responseTransformer && isSuccess(response.status)) {
|
||||
transformedBody = await options.responseTransformer(responseBody);
|
||||
}
|
||||
|
||||
const result: ApiResult = {
|
||||
url,
|
||||
ok: isSuccess(response.status),
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
body: responseHeader ?? transformedBody,
|
||||
};
|
||||
|
||||
catchErrorCodes(options, result);
|
||||
|
||||
resolve(result.body);
|
||||
}
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
};
|
||||
118
www/app/api/core/types.ts
Normal file
118
www/app/api/core/types.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import type { Auth, AuthToken } from "./auth";
|
||||
import type {
|
||||
BodySerializer,
|
||||
QuerySerializer,
|
||||
QuerySerializerOptions,
|
||||
} from "./bodySerializer";
|
||||
|
||||
export interface Client<
|
||||
RequestFn = never,
|
||||
Config = unknown,
|
||||
MethodFn = never,
|
||||
BuildUrlFn = never,
|
||||
> {
|
||||
/**
|
||||
* Returns the final request URL.
|
||||
*/
|
||||
buildUrl: BuildUrlFn;
|
||||
connect: MethodFn;
|
||||
delete: MethodFn;
|
||||
get: MethodFn;
|
||||
getConfig: () => Config;
|
||||
head: MethodFn;
|
||||
options: MethodFn;
|
||||
patch: MethodFn;
|
||||
post: MethodFn;
|
||||
put: MethodFn;
|
||||
request: RequestFn;
|
||||
setConfig: (config: Config) => Config;
|
||||
trace: MethodFn;
|
||||
}
|
||||
|
||||
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) | 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?:
|
||||
| "CONNECT"
|
||||
| "DELETE"
|
||||
| "GET"
|
||||
| "HEAD"
|
||||
| "OPTIONS"
|
||||
| "PATCH"
|
||||
| "POST"
|
||||
| "PUT"
|
||||
| "TRACE";
|
||||
/**
|
||||
* 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<unknown>;
|
||||
/**
|
||||
* 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<unknown>;
|
||||
/**
|
||||
* 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<unknown>;
|
||||
}
|
||||
|
||||
type IsExactlyNeverOrNeverUndefined<T> = [T] extends [never]
|
||||
? true
|
||||
: [T] extends [never | undefined]
|
||||
? [undefined] extends [T]
|
||||
? false
|
||||
: true
|
||||
: false;
|
||||
|
||||
export type OmitNever<T extends Record<string, unknown>> = {
|
||||
[K in keyof T as IsExactlyNeverOrNeverUndefined<T[K]> extends true
|
||||
? never
|
||||
: K]: T[K];
|
||||
};
|
||||
Reference in New Issue
Block a user