diff --git a/src/model.ts b/src/model.ts index 4a37fddf8..60c7f486e 100644 --- a/src/model.ts +++ b/src/model.ts @@ -8,4 +8,39 @@ interface Subtheme { name: string } -export type { Theme, Subtheme } +interface Request { + method: string + url: string + data?: Record +} + +interface Response { + status: number + data?: Record + error?: { message: string } +} + +interface DiscussionParams { + title: string + comment: string + subject: DiscussionSubject +} + +interface DiscussionSubject { + class: 'Topic' + id: string +} + +interface DiscussionResponse extends Response { + status: 200 | number + data?: Partial +} + +export type { + Theme, + Subtheme, + Request, + Response, + DiscussionParams, + DiscussionResponse +} diff --git a/src/services/api/DatagouvfrAPI.js b/src/services/api/DatagouvfrAPI.ts similarity index 77% rename from src/services/api/DatagouvfrAPI.js rename to src/services/api/DatagouvfrAPI.ts index ef463def4..1fa4c2abf 100644 --- a/src/services/api/DatagouvfrAPI.js +++ b/src/services/api/DatagouvfrAPI.ts @@ -3,7 +3,9 @@ import { toast } from 'vue3-toastify' import { useLoading } from 'vue-loading-overlay' import config from '@/config' +import type { Response } from '@/model' +// FIXME: the client should not depend be aware of the store. import { useUserStore } from '../../store/UserStore' const $loading = useLoading() @@ -21,7 +23,7 @@ instance.interceptors.request.use( return config }, - (error) => Promise.reject(error) + async (error) => await Promise.reject(error) ) /** @@ -73,14 +75,16 @@ export default class DatagouvfrAPI { */ async makeRequestAndHandleResponse(url, method = 'get', params = {}) { const loader = $loading.show() - return this.request(url, method, params) + return await this.request(url, method, params) .catch((error) => { - if (error && error.message) { + if (error?.message) { toast(error.message, { type: 'error', autoClose: false }) // TODO: Refacto to handle the error return error.response } }) - .finally(() => loader.hide()) + .finally(() => { + loader.hide() + }) } /** @@ -141,6 +145,23 @@ export default class DatagouvfrAPI { ) } + /** + * Base function for HTTP calls (without error handling) + * + * @todo Remove this function once all API calls are all handled this way: + * leaving the error handling to the caller (store). + * + * @param {string} url + * @param {object} data + * @returns {Promise} + */ + async _create(url: string, data = {}): Promise { + return await this.httpClient.post(url, data).then( + (response) => response, + (error) => this.#handleError(error) + ) + } + /** * Update an entity (PUT) * @@ -160,17 +181,19 @@ export default class DatagouvfrAPI { * Delete an entity (DELETE) * * @param {string} entityId - A UUID entity id - * @returns {Promise} + * @returns {Promise} */ - async delete(entityId) { - return this.httpClient.delete(`${this.url()}/${entityId}/`).then( + async delete(entityId: string): Promise { + return await this.httpClient.delete(`${this.url()}/${entityId}/`).then( (response) => response, (error) => this.#handleError(error) ) } - #handleError({ response, message }) { - if (response) return { status: response.status } - return { message } + #handleError({ response, message }): Response { + return { + ...(response && { status: response.status }), + ...(message && { error: { message } }) + } } } diff --git a/src/services/api/__tests__/DatagouvfrAPI.test.js b/src/services/api/__tests__/DatagouvfrAPI.test.js index 74d751de4..fdafe271e 100644 --- a/src/services/api/__tests__/DatagouvfrAPI.test.js +++ b/src/services/api/__tests__/DatagouvfrAPI.test.js @@ -80,8 +80,8 @@ test('delete when 404', async ({ client }) => { }) test('delete something else', async ({ client }) => { - const { message } = await client.delete(networkError) - expect(message).toMatch(/network error/i) + const { error } = await client.delete(networkError) + expect(error.message).toMatch(/network error/i) }) test('raw list', async ({ client }) => { diff --git a/src/services/api/__tests__/DiscussionsAPI.test.js b/src/services/api/__tests__/DiscussionsAPI.test.js new file mode 100644 index 000000000..9269d7bfb --- /dev/null +++ b/src/services/api/__tests__/DiscussionsAPI.test.js @@ -0,0 +1,61 @@ +import axios from 'axios' +import { HttpResponse, http } from 'msw' +import { setupServer } from 'msw/node' +import { createPinia, setActivePinia } from 'pinia' +import { + afterAll, + afterEach, + beforeAll, + beforeEach, + expect, + test +} from 'vitest' + +import DiscussionsAPI from '@/services/api/resources/DiscussionsAPI' + +const baseUrl = 'https://example.lol' +const version = '1234' +const endpoint = 'discussions' + +const discussionRequest = { + title: 'Title of the discussion', + comment: 'This is a discussion.', + subject: { + class: 'Topic', + id: 'id123' + } +} + +const server = setupServer( + http.post(`${baseUrl}/${version}/${endpoint}/`, () => { + return HttpResponse.json(discussionRequest, { status: 200 }) + }) +) + +beforeAll(() => { + server.listen() +}) + +beforeEach(async (context) => { + const httpClient = axios.create() + httpClient.defaults.proxy = false + setActivePinia(createPinia()) + context.client = new DiscussionsAPI({ + baseUrl, + version, + httpClient + }) +}) + +afterEach(() => { + server.resetHandlers() +}) + +afterAll(() => { + server.close() +}) + +test('create a discussion', async ({ client }) => { + const { data } = await client.create(discussionRequest) + expect(data.title).toEqual(discussionRequest.title) +}) diff --git a/src/services/api/resources/DiscussionsAPI.js b/src/services/api/resources/DiscussionsAPI.ts similarity index 57% rename from src/services/api/resources/DiscussionsAPI.js rename to src/services/api/resources/DiscussionsAPI.ts index 6d94e4ecd..fad5ada72 100644 --- a/src/services/api/resources/DiscussionsAPI.js +++ b/src/services/api/resources/DiscussionsAPI.ts @@ -1,3 +1,5 @@ +import type { DiscussionParams, DiscussionResponse } from '@/model' + import DatagouvfrAPI from '../DatagouvfrAPI' export default class DiscussionsAPI extends DatagouvfrAPI { @@ -14,4 +16,14 @@ export default class DiscussionsAPI extends DatagouvfrAPI { const url = `${this.url()}/?for=${dataset_id}&page=${page}` return await this.makeRequestAndHandleResponse(url) } + + /** + * Create a discussion (POST) + * + * @param {DiscussionParams} data + * @returns {Promise} + */ + async create(data: DiscussionParams): Promise { + return await this._create(`${this.url()}/`, data) + } }