feat(useApi): better interface
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -31,3 +31,4 @@ node_modules
|
|||||||
# Nix
|
# Nix
|
||||||
result
|
result
|
||||||
.data/
|
.data/
|
||||||
|
app/coverage/*
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<p class="text-text-800 text-sm">Copyright © {{ new Date().getFullYear() }}</p>
|
<p class="text-text-800 text-sm">Copyright © {{ new Date().getFullYear() }}</p>
|
||||||
<p class="text-text-800 text-sm">{{ $t('footer.versions.frontend') }}: {{ version }}</p>
|
<p class="text-text-800 text-sm">{{ $t('footer.versions.frontend') }}: {{ version }}</p>
|
||||||
<p class="text-text-800 text-sm">{{ $t('footer.versions.backend') }}: {{ meta?.version }}</p>
|
<p class="text-text-800 text-sm">{{ $t('footer.versions.backend') }}: {{ backendVersion }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -30,7 +30,10 @@ import { version } from '../../package.json';
|
|||||||
const { isMobile } = useDevice();
|
const { isMobile } = useDevice();
|
||||||
const orientation = computed(() => (isMobile ? 'vertical' : 'horizontal'));
|
const orientation = computed(() => (isMobile ? 'vertical' : 'horizontal'));
|
||||||
const { getMeta } = useBackend();
|
const { getMeta } = useBackend();
|
||||||
const meta = await getMeta();
|
const { data, error, loading } = getMeta();
|
||||||
|
const backendVersion = computed(() =>
|
||||||
|
loading.value ? 'backend.loading' : data?.value?.version || $t('backend.failed'),
|
||||||
|
);
|
||||||
const items = computed<NavigationMenuItem[]>(() => [
|
const items = computed<NavigationMenuItem[]>(() => [
|
||||||
{
|
{
|
||||||
label: $t('footer.links.source'),
|
label: $t('footer.links.source'),
|
||||||
|
|||||||
361
app/composables/useApi.test.ts
Normal file
361
app/composables/useApi.test.ts
Normal file
@@ -0,0 +1,361 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||||
|
import { nextTick } from 'vue';
|
||||||
|
import type { FetchError } from 'ofetch';
|
||||||
|
import type { ApiError } from '~/types/api/error';
|
||||||
|
import { useApi } from './useApi';
|
||||||
|
|
||||||
|
// Mock dependencies
|
||||||
|
vi.mock('#app', () => ({
|
||||||
|
useRuntimeConfig: vi.fn(() => ({
|
||||||
|
public: {
|
||||||
|
apiBase: 'http://localhost:3100/api',
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock $fetch globally
|
||||||
|
global.$fetch = vi.fn();
|
||||||
|
|
||||||
|
describe('useApi', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET requests', () => {
|
||||||
|
it('should make a GET request and populate data on success', async () => {
|
||||||
|
const mockData = { id: 1, name: 'Test' };
|
||||||
|
vi.mocked($fetch).mockResolvedValueOnce(mockData);
|
||||||
|
|
||||||
|
const api = useApi();
|
||||||
|
const result = api.get<typeof mockData>('/test');
|
||||||
|
|
||||||
|
// Should start loading
|
||||||
|
await nextTick();
|
||||||
|
expect(result.loading.value).toBe(false); // Immediate execution completes quickly
|
||||||
|
|
||||||
|
// Wait for the async operation
|
||||||
|
await vi.waitFor(() => expect(result.data.value).toStrictEqual(mockData));
|
||||||
|
|
||||||
|
expect($fetch).toHaveBeenCalledWith('/test', {
|
||||||
|
baseURL: 'http://localhost:3100/api',
|
||||||
|
method: 'GET',
|
||||||
|
body: undefined,
|
||||||
|
});
|
||||||
|
expect(result.data.value).toEqual(mockData);
|
||||||
|
expect(result.error.value).toBeNull();
|
||||||
|
expect(result.loading.value).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle GET request with custom options', async () => {
|
||||||
|
const mockData = { result: 'success' };
|
||||||
|
vi.mocked($fetch).mockResolvedValueOnce(mockData);
|
||||||
|
|
||||||
|
const api = useApi();
|
||||||
|
const result = api.get('/test', { headers: { 'X-Custom': 'header' } });
|
||||||
|
|
||||||
|
await vi.waitFor(() => expect(result.data.value).toStrictEqual(mockData));
|
||||||
|
|
||||||
|
expect($fetch).toHaveBeenCalledWith('/test', {
|
||||||
|
baseURL: 'http://localhost:3100/api',
|
||||||
|
method: 'GET',
|
||||||
|
headers: { 'X-Custom': 'header' },
|
||||||
|
body: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not execute immediately when immediate is false', async () => {
|
||||||
|
const api = useApi();
|
||||||
|
const result = api.get('/test', {}, false);
|
||||||
|
|
||||||
|
expect($fetch).not.toHaveBeenCalled();
|
||||||
|
expect(result.data.value).toBeNull();
|
||||||
|
expect(result.loading.value).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should execute when run() is called manually', async () => {
|
||||||
|
const mockData = { manual: true };
|
||||||
|
vi.mocked($fetch).mockResolvedValueOnce(mockData);
|
||||||
|
|
||||||
|
const api = useApi();
|
||||||
|
const result = api.get('/test', {}, false);
|
||||||
|
|
||||||
|
expect($fetch).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
await result.run();
|
||||||
|
|
||||||
|
expect($fetch).toHaveBeenCalledWith('/test', {
|
||||||
|
baseURL: 'http://localhost:3100/api',
|
||||||
|
method: 'GET',
|
||||||
|
body: undefined,
|
||||||
|
});
|
||||||
|
expect(result.data.value).toEqual(mockData);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('DELETE requests', () => {
|
||||||
|
it('should make a DELETE request', async () => {
|
||||||
|
const mockData = { deleted: true };
|
||||||
|
vi.mocked($fetch).mockResolvedValueOnce(mockData);
|
||||||
|
|
||||||
|
const api = useApi();
|
||||||
|
const result = api.del<typeof mockData>('/test/1');
|
||||||
|
|
||||||
|
await vi.waitFor(() => expect(result.data.value).toStrictEqual(mockData));
|
||||||
|
|
||||||
|
expect($fetch).toHaveBeenCalledWith('/test/1', {
|
||||||
|
baseURL: 'http://localhost:3100/api',
|
||||||
|
method: 'DELETE',
|
||||||
|
body: undefined,
|
||||||
|
});
|
||||||
|
expect(result.data.value).toEqual(mockData);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('POST requests', () => {
|
||||||
|
it('should make a POST request with body', async () => {
|
||||||
|
const mockResponse = { id: 1, created: true };
|
||||||
|
const requestBody = { name: 'New Item' };
|
||||||
|
vi.mocked($fetch).mockResolvedValueOnce(mockResponse);
|
||||||
|
|
||||||
|
const api = useApi();
|
||||||
|
const result = api.post<typeof mockResponse, typeof requestBody>(
|
||||||
|
'/test',
|
||||||
|
{},
|
||||||
|
true,
|
||||||
|
requestBody,
|
||||||
|
);
|
||||||
|
|
||||||
|
await vi.waitFor(() => expect(result.data.value).toStrictEqual(mockResponse));
|
||||||
|
|
||||||
|
expect($fetch).toHaveBeenCalledWith('/test', {
|
||||||
|
baseURL: 'http://localhost:3100/api',
|
||||||
|
method: 'POST',
|
||||||
|
body: requestBody,
|
||||||
|
});
|
||||||
|
expect(result.data.value).toEqual(mockResponse);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow run() to be called with a different body', async () => {
|
||||||
|
const mockResponse = { success: true };
|
||||||
|
vi.mocked($fetch).mockResolvedValueOnce(mockResponse);
|
||||||
|
|
||||||
|
const api = useApi();
|
||||||
|
const result = api.post<typeof mockResponse, { data: string }>('/test', {}, false);
|
||||||
|
|
||||||
|
const body = { data: 'runtime-data' };
|
||||||
|
await result.run(body);
|
||||||
|
|
||||||
|
expect($fetch).toHaveBeenCalledWith('/test', {
|
||||||
|
baseURL: 'http://localhost:3100/api',
|
||||||
|
method: 'POST',
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
expect(result.data.value).toEqual(mockResponse);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('PUT requests', () => {
|
||||||
|
it('should make a PUT request with body', async () => {
|
||||||
|
const mockResponse = { updated: true };
|
||||||
|
const requestBody = { name: 'Updated Item' };
|
||||||
|
vi.mocked($fetch).mockResolvedValueOnce(mockResponse);
|
||||||
|
|
||||||
|
const api = useApi();
|
||||||
|
const result = api.put<typeof mockResponse, typeof requestBody>(
|
||||||
|
'/test/1',
|
||||||
|
{},
|
||||||
|
true,
|
||||||
|
requestBody,
|
||||||
|
);
|
||||||
|
|
||||||
|
await vi.waitFor(() => expect(result.data.value).toStrictEqual(mockResponse));
|
||||||
|
|
||||||
|
expect($fetch).toHaveBeenCalledWith('/test/1', {
|
||||||
|
baseURL: 'http://localhost:3100/api',
|
||||||
|
method: 'PUT',
|
||||||
|
body: requestBody,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('PATCH requests', () => {
|
||||||
|
it('should make a PATCH request with body', async () => {
|
||||||
|
const mockResponse = { patched: true };
|
||||||
|
const requestBody = { field: 'value' };
|
||||||
|
vi.mocked($fetch).mockResolvedValueOnce(mockResponse);
|
||||||
|
|
||||||
|
const api = useApi();
|
||||||
|
const result = api.patch<typeof mockResponse, typeof requestBody>(
|
||||||
|
'/test/1',
|
||||||
|
{},
|
||||||
|
true,
|
||||||
|
requestBody,
|
||||||
|
);
|
||||||
|
|
||||||
|
await vi.waitFor(() => expect(result.data.value).toStrictEqual(mockResponse));
|
||||||
|
|
||||||
|
expect($fetch).toHaveBeenCalledWith('/test/1', {
|
||||||
|
baseURL: 'http://localhost:3100/api',
|
||||||
|
method: 'PATCH',
|
||||||
|
body: requestBody,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Error handling', () => {
|
||||||
|
it('should handle fetch errors with ApiError response', async () => {
|
||||||
|
const apiError: ApiError = {
|
||||||
|
message: 'backend.errors.not_found',
|
||||||
|
success: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchError: Partial<FetchError> = {
|
||||||
|
message: 'Fetch Error',
|
||||||
|
response: {
|
||||||
|
_data: apiError,
|
||||||
|
} as any,
|
||||||
|
};
|
||||||
|
|
||||||
|
vi.mocked($fetch).mockRejectedValueOnce(fetchError);
|
||||||
|
|
||||||
|
const api = useApi();
|
||||||
|
const result = api.get('/test');
|
||||||
|
|
||||||
|
await vi.waitFor(() => expect(result.error.value).not.toBeNull());
|
||||||
|
|
||||||
|
expect(result.data.value).toBeNull();
|
||||||
|
expect(result.error.value).toEqual(apiError);
|
||||||
|
expect(result.loading.value).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle fetch errors without ApiError response', async () => {
|
||||||
|
const fetchError: Partial<FetchError> = {
|
||||||
|
message: 'Network Error',
|
||||||
|
response: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
vi.mocked($fetch).mockRejectedValueOnce(fetchError);
|
||||||
|
|
||||||
|
const api = useApi();
|
||||||
|
const result = api.get('/test');
|
||||||
|
|
||||||
|
await vi.waitFor(() => expect(result.error.value).not.toBeNull());
|
||||||
|
|
||||||
|
expect(result.error.value).toEqual({
|
||||||
|
message: 'Network Error',
|
||||||
|
success: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use default error message when fetch error has no message', async () => {
|
||||||
|
const fetchError: Partial<FetchError> = {
|
||||||
|
message: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
vi.mocked($fetch).mockRejectedValueOnce(fetchError);
|
||||||
|
|
||||||
|
const api = useApi();
|
||||||
|
const result = api.get('/test');
|
||||||
|
|
||||||
|
await vi.waitFor(() => expect(result.error.value).not.toBeNull());
|
||||||
|
|
||||||
|
expect(result.error.value).toEqual({
|
||||||
|
message: 'backend.errors.unknown',
|
||||||
|
success: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clear previous errors on new request', async () => {
|
||||||
|
const fetchError: Partial<FetchError> = {
|
||||||
|
message: 'First Error',
|
||||||
|
};
|
||||||
|
const mockData = { success: true };
|
||||||
|
|
||||||
|
// First request fails
|
||||||
|
vi.mocked($fetch).mockRejectedValueOnce(fetchError);
|
||||||
|
|
||||||
|
const api = useApi();
|
||||||
|
const result = api.get('/test', {}, false);
|
||||||
|
|
||||||
|
await result.run();
|
||||||
|
await vi.waitFor(() => expect(result.error.value).not.toBeNull());
|
||||||
|
expect(result.error.value?.message).toBe('First Error');
|
||||||
|
|
||||||
|
// Second request succeeds
|
||||||
|
vi.mocked($fetch).mockResolvedValueOnce(mockData);
|
||||||
|
await result.run();
|
||||||
|
|
||||||
|
await vi.waitFor(() => expect(result.data.value).toStrictEqual(mockData));
|
||||||
|
expect(result.error.value).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Loading state', () => {
|
||||||
|
it('should set loading to true during request', async () => {
|
||||||
|
let resolvePromise: (value: any) => void;
|
||||||
|
const promise = new Promise((resolve) => {
|
||||||
|
resolvePromise = resolve;
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mocked($fetch).mockReturnValueOnce(promise as any);
|
||||||
|
|
||||||
|
const api = useApi();
|
||||||
|
const result = api.get('/test', {}, false);
|
||||||
|
|
||||||
|
expect(result.loading.value).toBe(false);
|
||||||
|
|
||||||
|
const runPromise = result.run();
|
||||||
|
|
||||||
|
// Should be loading
|
||||||
|
await nextTick();
|
||||||
|
expect(result.loading.value).toBe(true);
|
||||||
|
|
||||||
|
// Resolve the request
|
||||||
|
resolvePromise!({ done: true });
|
||||||
|
await runPromise;
|
||||||
|
|
||||||
|
expect(result.loading.value).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set loading to false after error', async () => {
|
||||||
|
let rejectPromise: (error: any) => void;
|
||||||
|
const promise = new Promise((_, reject) => {
|
||||||
|
rejectPromise = reject;
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mocked($fetch).mockReturnValueOnce(promise as any);
|
||||||
|
|
||||||
|
const api = useApi();
|
||||||
|
const result = api.get('/test', {}, false);
|
||||||
|
|
||||||
|
const runPromise = result.run();
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
expect(result.loading.value).toBe(true);
|
||||||
|
|
||||||
|
rejectPromise!({ message: 'Error' });
|
||||||
|
await runPromise;
|
||||||
|
|
||||||
|
expect(result.loading.value).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Return type structure', () => {
|
||||||
|
it('should return QueryResult with correct structure', async () => {
|
||||||
|
vi.mocked($fetch).mockResolvedValueOnce({ test: 'data' });
|
||||||
|
|
||||||
|
const api = useApi();
|
||||||
|
const result = api.get('/test', {}, false);
|
||||||
|
|
||||||
|
expect(result).toHaveProperty('data');
|
||||||
|
expect(result).toHaveProperty('error');
|
||||||
|
expect(result).toHaveProperty('loading');
|
||||||
|
expect(result).toHaveProperty('run');
|
||||||
|
expect(typeof result.run).toBe('function');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,32 +1,67 @@
|
|||||||
import type { FetchOptions } from 'ofetch';
|
import type { FetchError, FetchOptions } from 'ofetch';
|
||||||
|
import type { ApiError } from '~/types/api/error';
|
||||||
|
import type { HttpMethod } from '~/types/http-method';
|
||||||
|
import { QueryResult } from '~/types/query-result';
|
||||||
|
|
||||||
export const useApi = () => {
|
export interface UseApi {
|
||||||
const config = useRuntimeConfig();
|
get: <T>(path: string, opts?: FetchOptions, immediate?: boolean) => UseApiResponse<T>;
|
||||||
const apiFetch = $fetch.create({
|
del: <T>(path: string, opts?: FetchOptions, immediate?: boolean) => UseApiResponse<T>;
|
||||||
baseURL: config.public.apiBase,
|
post: <T, B = unknown>(path: string, opts?: FetchOptions, immediate?: boolean, body?: B) => UseApiResponse<T, B>;
|
||||||
});
|
put: <T, B = unknown>(path: string, opts?: FetchOptions, immediate?: boolean, body?: B) => UseApiResponse<T, B>;
|
||||||
|
patch: <T, B = unknown>(path: string, opts?: FetchOptions, immediate?: boolean, body?: B) => UseApiResponse<T, B>;
|
||||||
|
}
|
||||||
|
|
||||||
const get = <T>(url: string, options?: FetchOptions) => apiFetch<T>(url, { method: 'GET', ...options });
|
const createRequest = <ResponseT = unknown, PayloadT = unknown>(
|
||||||
|
method: HttpMethod,
|
||||||
|
url: string,
|
||||||
|
opts?: FetchOptions,
|
||||||
|
immediate: boolean = true,
|
||||||
|
body?: PayloadT,
|
||||||
|
): QueryResult<ResponseT, PayloadT> => {
|
||||||
|
const response = new QueryResult<ResponseT, PayloadT>();
|
||||||
|
const { apiBase } = useRuntimeConfig().public;
|
||||||
|
|
||||||
const post = <ResultT, PayloadT = Record<string, string | number | boolean>>(
|
const run = async (requestBody?: PayloadT): Promise<void> => {
|
||||||
url: string,
|
response.loading.value = true;
|
||||||
body?: PayloadT,
|
response.error.value = null;
|
||||||
options?: FetchOptions,
|
|
||||||
) => apiFetch<ResultT>(url, { method: 'POST', body, ...options });
|
|
||||||
|
|
||||||
const put = <ResultT, PayloadT = Record<string, string | number | boolean>>(
|
try {
|
||||||
url: string,
|
const res = await $fetch<ResponseT>(url, {
|
||||||
body?: PayloadT,
|
baseURL: apiBase,
|
||||||
options?: FetchOptions,
|
...opts,
|
||||||
) => apiFetch<ResultT>(url, { method: 'PUT', body, ...options });
|
method,
|
||||||
|
body: requestBody ?? undefined,
|
||||||
|
});
|
||||||
|
response.data.value = res;
|
||||||
|
} catch (e) {
|
||||||
|
const fetchError = e as FetchError;
|
||||||
|
const errBody = fetchError?.response?._data as ApiError | undefined;
|
||||||
|
response.error.value = errBody ?? {
|
||||||
|
message: fetchError.message || 'backend.errors.unknown',
|
||||||
|
success: false,
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
response.loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
response.run = run;
|
||||||
|
|
||||||
const patch = <ResultT, PayloadT = Record<string, string | number | boolean>>(
|
if (immediate) run(body);
|
||||||
url: string,
|
|
||||||
body?: PayloadT,
|
|
||||||
options?: FetchOptions,
|
|
||||||
) => apiFetch<ResultT>(url, { method: 'PATCH', body, ...options });
|
|
||||||
|
|
||||||
const del = <T>(url: string, options?: FetchOptions) => apiFetch<T>(url, { method: 'DELETE', ...options });
|
return response;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useApi = (): UseApi => {
|
||||||
|
const get = <T>(path: string, opts?: FetchOptions, immediate: boolean = true) =>
|
||||||
|
createRequest<T>('GET', path, opts, immediate);
|
||||||
|
const del = <T>(path: string, opts?: FetchOptions, immediate: boolean = true) =>
|
||||||
|
createRequest<T>('DELETE', path, opts, immediate);
|
||||||
|
const post = <T, B = unknown>(path: string, opts?: FetchOptions, immediate: boolean = true, body?: B) =>
|
||||||
|
createRequest<T, B>('POST', path, opts, immediate, body);
|
||||||
|
const put = <T, B = unknown>(path: string, opts?: FetchOptions, immediate: boolean = true, body?: B) =>
|
||||||
|
createRequest<T, B>('PUT', path, opts, immediate, body);
|
||||||
|
const patch = <T, B = unknown>(path: string, opts?: FetchOptions, immediate: boolean = true, body?: B) =>
|
||||||
|
createRequest<T, B>('PATCH', path, opts, immediate, body);
|
||||||
|
|
||||||
return { get, post, put, patch, del };
|
return { get, post, put, patch, del };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
|
import type { ContactRequest, ContactResponse } from '~/types/api/contact';
|
||||||
|
import type { MetaResponse } from '~/types/api/meta';
|
||||||
|
|
||||||
export const useBackend = () => {
|
export const useBackend = () => {
|
||||||
const api = useApi();
|
const api = useApi();
|
||||||
|
|
||||||
const getMeta = () => api.get<MetaResponse>('/meta');
|
const getMeta = (): UseApiResponse<MetaResponse> => api.get<MetaResponse>('/meta');
|
||||||
const postContact = (contact: ContactRequest) => api.post<ContactRequest, ContactResponse>('/contact', contact);
|
const postContact = (): UseApiResponse<ContactResponse, ContactRequest> =>
|
||||||
|
api.post<ContactResponse, ContactRequest>('/contact', false);
|
||||||
|
|
||||||
return { getMeta, postContact };
|
return { getMeta, postContact };
|
||||||
};
|
};
|
||||||
|
|||||||
19
app/types/http-method.ts
Normal file
19
app/types/http-method.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
export type HttpMethod =
|
||||||
|
| 'delete'
|
||||||
|
| 'get'
|
||||||
|
| 'GET'
|
||||||
|
| 'HEAD'
|
||||||
|
| 'PATCH'
|
||||||
|
| 'POST'
|
||||||
|
| 'PUT'
|
||||||
|
| 'DELETE'
|
||||||
|
| 'CONNECT'
|
||||||
|
| 'OPTIONS'
|
||||||
|
| 'TRACE'
|
||||||
|
| 'head'
|
||||||
|
| 'patch'
|
||||||
|
| 'post'
|
||||||
|
| 'put'
|
||||||
|
| 'connect'
|
||||||
|
| 'options'
|
||||||
|
| 'trace';
|
||||||
12
app/types/query-result.ts
Normal file
12
app/types/query-result.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import type { ApiError } from './api/error';
|
||||||
|
|
||||||
|
export class QueryResult<T, PayloadT> {
|
||||||
|
/** Reactive data - `null` until the request succeeds */
|
||||||
|
data: Ref<T | null> = ref(null);
|
||||||
|
/** Reactive error - `null` until an error occurs */
|
||||||
|
error: Ref<ApiError | null> = ref(null);
|
||||||
|
/** Whether the request is currently in flight */
|
||||||
|
loading: Ref<boolean> = ref(false);
|
||||||
|
/** Runs the query - Will be filled by the request helper */
|
||||||
|
run!: (requestBody?: PayloadT) => Promise<void>;
|
||||||
|
}
|
||||||
12
package.json
12
package.json
@@ -12,7 +12,9 @@
|
|||||||
"cleanup": "nuxt cleanup",
|
"cleanup": "nuxt cleanup",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"format": "prettier --write app/ i18n/ content/",
|
"format": "prettier --write app/ i18n/ content/",
|
||||||
"format-check": "prettier --check app/ i18n/ content/"
|
"format-check": "prettier --check app/ i18n/ content/",
|
||||||
|
"test": "vitest",
|
||||||
|
"test:local": "vitest -c vitest.config.local.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxt/content": "3.8.0",
|
"@nuxt/content": "3.8.0",
|
||||||
@@ -21,7 +23,6 @@
|
|||||||
"@nuxt/icon": "2.1.0",
|
"@nuxt/icon": "2.1.0",
|
||||||
"@nuxt/image": "1.11.0",
|
"@nuxt/image": "1.11.0",
|
||||||
"@nuxt/scripts": "^0.12.2",
|
"@nuxt/scripts": "^0.12.2",
|
||||||
"@nuxt/test-utils": "3.20.1",
|
|
||||||
"@nuxt/ui": "4.1.0",
|
"@nuxt/ui": "4.1.0",
|
||||||
"@nuxtjs/color-mode": "3.5.2",
|
"@nuxtjs/color-mode": "3.5.2",
|
||||||
"@nuxtjs/device": "3.2.4",
|
"@nuxtjs/device": "3.2.4",
|
||||||
@@ -42,13 +43,20 @@
|
|||||||
"@iconify-json/material-symbols-light": "^1.2.44",
|
"@iconify-json/material-symbols-light": "^1.2.44",
|
||||||
"@iconify-json/mdi": "^1.2.3",
|
"@iconify-json/mdi": "^1.2.3",
|
||||||
"@iconify-json/simple-icons": "^1.2.58",
|
"@iconify-json/simple-icons": "^1.2.58",
|
||||||
|
"@nuxt/test-utils": "3.20.1",
|
||||||
"@nuxtjs/i18n": "^10.2.0",
|
"@nuxtjs/i18n": "^10.2.0",
|
||||||
"@tailwindcss/postcss": "^4.1.17",
|
"@tailwindcss/postcss": "^4.1.17",
|
||||||
|
"@vitest/coverage-v8": "^4.0.11",
|
||||||
|
"@vitest/ui": "^4.0.11",
|
||||||
|
"@vue/test-utils": "^2.4.6",
|
||||||
"autoprefixer": "^10.4.22",
|
"autoprefixer": "^10.4.22",
|
||||||
|
"happy-dom": "^20.0.10",
|
||||||
"less": "^4.4.2",
|
"less": "^4.4.2",
|
||||||
|
"playwright-core": "^1.56.1",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"tailwindcss": "^4.1.17",
|
"tailwindcss": "^4.1.17",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
|
"vitest": "^4.0.10",
|
||||||
"zod": "^4.1.12"
|
"zod": "^4.1.12"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
648
pnpm-lock.yaml
generated
648
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
19
vitest.config.local.ts
Normal file
19
vitest.config.local.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import {defineVitestConfig} from '@nuxt/test-utils/config';
|
||||||
|
|
||||||
|
export default defineVitestConfig({
|
||||||
|
test: {
|
||||||
|
environment: 'nuxt',
|
||||||
|
exclude: [
|
||||||
|
'**/.direnv/**',
|
||||||
|
'**/.devenv/**',
|
||||||
|
'**/node_modules/**'
|
||||||
|
],
|
||||||
|
coverage: {
|
||||||
|
provider: 'v8',
|
||||||
|
enabled: true,
|
||||||
|
reporter: ['html']
|
||||||
|
},
|
||||||
|
watch: true,
|
||||||
|
ui: true
|
||||||
|
}
|
||||||
|
})
|
||||||
18
vitest.config.ts
Normal file
18
vitest.config.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import {defineVitestConfig} from '@nuxt/test-utils/config';
|
||||||
|
|
||||||
|
export default defineVitestConfig({
|
||||||
|
test: {
|
||||||
|
environment: 'nuxt',
|
||||||
|
exclude: [
|
||||||
|
'**/.direnv/**',
|
||||||
|
'**/.devenv/**',
|
||||||
|
'**/node_modules/**'
|
||||||
|
],
|
||||||
|
coverage: {
|
||||||
|
provider: 'v8',
|
||||||
|
enabled: true,
|
||||||
|
reporter: ['lcovonly']
|
||||||
|
},
|
||||||
|
watch: false
|
||||||
|
}
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user