217 lines
5.8 KiB
TypeScript
217 lines
5.8 KiB
TypeScript
import { Inject, Injectable } from '@angular/core';
|
|
import { WINDOW } from '@ng-web-apis/common';
|
|
import { ApiResponseSchema } from 'picsur-shared/dist/dto/api/api.dto';
|
|
import { FileType2Ext } from 'picsur-shared/dist/dto/mimes.dto';
|
|
import {
|
|
AsyncFailable,
|
|
Fail,
|
|
FT,
|
|
HasFailed,
|
|
HasSuccess,
|
|
} from 'picsur-shared/dist/types';
|
|
import { ZodDtoStatic } from 'picsur-shared/dist/util/create-zod-dto';
|
|
import { ParseMime2FileType } from 'picsur-shared/dist/util/parse-mime';
|
|
import { Subject } from 'rxjs';
|
|
import { ApiBuffer } from 'src/app/models/dto/api-buffer.dto';
|
|
import { ApiError } from 'src/app/models/dto/api-error.dto';
|
|
import { z } from 'zod';
|
|
import { MultiPartRequest } from '../../models/dto/multi-part-request.dto';
|
|
import { Logger } from '../logger/logger.service';
|
|
import { KeyService } from '../storage/key.service';
|
|
|
|
/*
|
|
Proud of this, it works so smoooth
|
|
*/
|
|
|
|
@Injectable({
|
|
providedIn: 'root',
|
|
})
|
|
export class ApiService {
|
|
private readonly logger = new Logger(ApiService.name);
|
|
|
|
private errorSubject = new Subject<ApiError>();
|
|
|
|
public get networkErrors() {
|
|
return this.errorSubject.asObservable();
|
|
}
|
|
|
|
constructor(
|
|
private readonly keyService: KeyService,
|
|
@Inject(WINDOW) private readonly windowRef: Window,
|
|
) {}
|
|
|
|
public async get<T extends z.AnyZodObject>(
|
|
type: ZodDtoStatic<T>,
|
|
url: string,
|
|
): AsyncFailable<z.infer<T>> {
|
|
return this.fetchSafeJson(type, url, { method: 'GET' });
|
|
}
|
|
|
|
public async head(url: string): AsyncFailable<Headers> {
|
|
return this.fetchHead(url, { method: 'HEAD' });
|
|
}
|
|
|
|
public async getBuffer(url: string): AsyncFailable<ApiBuffer> {
|
|
return this.fetchBuffer(url, { method: 'GET' });
|
|
}
|
|
|
|
public async post<T extends z.AnyZodObject, W extends z.AnyZodObject>(
|
|
sendType: ZodDtoStatic<T>,
|
|
receiveType: ZodDtoStatic<W>,
|
|
url: string,
|
|
data: z.infer<T>,
|
|
): AsyncFailable<z.infer<W>> {
|
|
const sendSchema = sendType.zodSchema;
|
|
|
|
const validateResult = sendSchema.safeParse(data);
|
|
if (!validateResult.success) {
|
|
return Fail(
|
|
FT.SysValidation,
|
|
'Something went wrong',
|
|
validateResult.error,
|
|
);
|
|
}
|
|
|
|
return this.fetchSafeJson(receiveType, url, {
|
|
method: 'POST',
|
|
body: JSON.stringify(validateResult.data),
|
|
});
|
|
}
|
|
|
|
public async postEmpty<T extends z.AnyZodObject>(
|
|
type: ZodDtoStatic<T>,
|
|
url: string,
|
|
): AsyncFailable<z.infer<T>> {
|
|
return this.fetchSafeJson(type, url, { method: 'POST' });
|
|
}
|
|
|
|
public async postForm<T extends z.AnyZodObject>(
|
|
receiveType: ZodDtoStatic<T>,
|
|
url: string,
|
|
data: MultiPartRequest,
|
|
): AsyncFailable<z.infer<T>> {
|
|
return this.fetchSafeJson(receiveType, url, {
|
|
method: 'POST',
|
|
body: data.createFormData(),
|
|
});
|
|
}
|
|
|
|
private async fetchSafeJson<T extends z.AnyZodObject>(
|
|
type: ZodDtoStatic<T>,
|
|
url: RequestInfo,
|
|
options: RequestInit,
|
|
): AsyncFailable<z.infer<T>> {
|
|
const resultSchema = ApiResponseSchema(type.zodSchema as z.AnyZodObject);
|
|
type resultType = z.infer<typeof resultSchema>;
|
|
|
|
let result = await this.fetchJsonAs<resultType>(url, options);
|
|
if (HasFailed(result)) return result;
|
|
|
|
const validateResult = resultSchema.safeParse(result);
|
|
if (!validateResult.success) {
|
|
return Fail(
|
|
FT.SysValidation,
|
|
'Something went wrong',
|
|
validateResult.error,
|
|
);
|
|
}
|
|
|
|
if (validateResult.data.success === false)
|
|
return Fail(FT.Unknown, result.data.message);
|
|
|
|
return validateResult.data.data;
|
|
}
|
|
|
|
private async fetchJsonAs<T>(
|
|
url: RequestInfo,
|
|
options: RequestInit,
|
|
): AsyncFailable<T> {
|
|
const response = await this.fetch(url, options);
|
|
if (HasFailed(response)) {
|
|
return response;
|
|
}
|
|
try {
|
|
return await response.json();
|
|
} catch (e) {
|
|
return Fail(FT.Internal, e);
|
|
}
|
|
}
|
|
|
|
private async fetchBuffer(
|
|
url: RequestInfo,
|
|
options: RequestInit,
|
|
): AsyncFailable<ApiBuffer> {
|
|
const response = await this.fetch(url, options);
|
|
if (HasFailed(response)) return response;
|
|
|
|
if (!response.ok) return Fail(FT.Network, 'Recieved a non-ok response');
|
|
|
|
const mimeType = response.headers.get('Content-Type') ?? 'other/unknown';
|
|
let name = response.headers.get('Content-Disposition');
|
|
if (!name) {
|
|
if (typeof url === 'string') {
|
|
name = url.split('/').pop() ?? 'unnamed';
|
|
} else {
|
|
name = url.url.split('/').pop() ?? 'unnamed';
|
|
}
|
|
}
|
|
|
|
const filetype = ParseMime2FileType(mimeType);
|
|
if (HasSuccess(filetype)) {
|
|
const ext = FileType2Ext(filetype.identifier);
|
|
if (HasSuccess(ext)) {
|
|
if (!name.endsWith(ext)) {
|
|
name += '.' + ext;
|
|
}
|
|
}
|
|
}
|
|
|
|
try {
|
|
const arrayBuffer = await response.arrayBuffer();
|
|
return {
|
|
buffer: arrayBuffer,
|
|
mimeType,
|
|
name,
|
|
};
|
|
} catch (e) {
|
|
return Fail(FT.Internal, e);
|
|
}
|
|
}
|
|
|
|
private async fetchHead(
|
|
url: RequestInfo,
|
|
options: RequestInit,
|
|
): AsyncFailable<Headers> {
|
|
const response = await this.fetch(url, options);
|
|
if (HasFailed(response)) return response;
|
|
|
|
if (!response.ok) return Fail(FT.Network, 'Recieved a non-ok response');
|
|
|
|
return response.headers;
|
|
}
|
|
|
|
private async fetch(
|
|
url: RequestInfo,
|
|
options: RequestInit,
|
|
): AsyncFailable<Response> {
|
|
try {
|
|
const key = this.keyService.get();
|
|
const isJSON = typeof options.body === 'string';
|
|
|
|
const headers: any = options.headers || {};
|
|
if (key !== null)
|
|
headers['Authorization'] = `Bearer ${this.keyService.get()}`;
|
|
if (isJSON) headers['Content-Type'] = 'application/json';
|
|
options.headers = headers;
|
|
|
|
return await this.windowRef.fetch(url, options);
|
|
} catch (e) {
|
|
this.errorSubject.next({
|
|
error: e,
|
|
url,
|
|
});
|
|
return Fail(FT.Network, e);
|
|
}
|
|
}
|
|
}
|