Picsur/frontend/src/app/services/api/api.service.ts

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);
}
}
}