/** * API module. * @module api */ import store from '@/store' import { openWebSocket, getResponseData, handleError } from './handlers' /** * Options available for an API call. * * @typedef {Object} Options * @property {Boolean} wait - If `true`, will display the waiting modal. * @property {Boolean} websocket - if `true`, will open a websocket connection. * @property {Boolean} initial - if `true` and an error occurs, the dismiss button will trigger a go back in history. * @property {Boolean} asFormData - if `true`, will send the data with a body encoded as `"multipart/form-data"` instead of `"x-www-form-urlencoded"`). */ /** * Representation of an API call for `api.fetchAll` * * @typedef {Array} Query * @property {String} 0 - "method" * @property {String|Object} 1 - "uri", uri to call as string or as an object for cached uris. * @property {Object|null} 2 - "data" * @property {Options} 3 - "options" */ /** * Converts an object literal into an `URLSearchParams` that can be turned into a * query string or used as a body in a `fetch` call. * * @param {Object} obj - An object literal to convert. * @param {Object} options * @param {Boolean} [options.addLocale=false] - Option to append the locale to the query string. * @return {URLSearchParams} */ export function objectToParams( obj, { addLocale = false } = {}, formData = false, ) { const urlParams = formData ? new FormData() : new URLSearchParams() for (const [key, value] of Object.entries(obj)) { if (Array.isArray(value)) { value.forEach((v) => urlParams.append(key, v)) } else { urlParams.append(key, value) } } if (addLocale) { urlParams.append('locale', store.getters.locale) } return urlParams } export default { options: { credentials: 'include', mode: 'cors', headers: { // FIXME is it important to keep this previous `Accept` header ? // 'Accept': 'application/json, text/javascript, */*; q=0.01', // Auto header is : // "Accept": "*/*", 'X-Requested-With': 'XMLHttpRequest', }, }, /** * Generic method to fetch the api without automatic response handling. * * @param {String} method - a method between 'GET', 'POST', 'PUT' and 'DELETE'. * @param {String} uri * @param {Object} [data={}] - data to send as body. * @param {Options} [options={ wait = true, websocket = true, initial = false, asFormData = false }] * @return {Promise} Promise that resolve the api response data or an error. */ async fetch( method, uri, data = {}, humanKey = null, { wait = true, websocket = true, initial = false, asFormData = false } = {}, ) { // `await` because Vuex actions returns promises by default. const request = await store.dispatch('INIT_REQUEST', { method, uri, humanKey, initial, wait, websocket, }) if (websocket) { await openWebSocket(request) } let options = this.options if (method === 'GET') { uri += `${uri.includes('?') ? '&' : '?'}locale=${store.getters.locale}` } else { options = { ...options, method, body: objectToParams(data, { addLocale: true }, true), } } const response = await fetch('/yunohost/api/' + uri, options) const responseData = await getResponseData(response) store.dispatch('END_REQUEST', { request, success: response.ok, wait }) return response.ok ? responseData : handleError(request, response, responseData) }, /** * Api multiple queries helper. * Those calls will act as one (declare optional waiting for one but still create history entries for each) * Calls are synchronous since the API can't handle multiple calls. * * @param {Array} queries - An array of queries with special representation. * @param {Object} [options={}] * @param {Boolean} * @return {Promise} Promise that resolve the api responses data or an error. */ async fetchAll(queries, { wait, initial } = {}) { const results = [] if (wait) store.commit('SET_WAITING', true) try { for (const [method, uri, data, humanKey, options = {}] of queries) { if (wait) options.wait = false if (initial) options.initial = true results.push( await this[method.toLowerCase()](uri, data, humanKey, options), ) } } finally { // Stop waiting even if there is an error. if (wait) store.commit('SET_WAITING', false) } return results }, /** * Api get helper function. * * @param {String|Object} uri * @param {null} [data=null] - for convenience in muliple calls, just pass null. * @param {Options} [options={}] - options to apply to the call (default is `{ websocket: false, wait: false }`) * @return {Promise} Promise that resolve the api response data or an error. */ get(uri, data = null, humanKey = null, options = {}) { options = { websocket: false, wait: false, ...options } if (typeof uri === 'string') return this.fetch('GET', uri, null, humanKey, options) return store.dispatch('GET', { ...uri, humanKey, options }) }, /** * Api post helper function. * * @param {String|Object} uri * @param {String} [data={}] - data to send as body. * @param {Options} [options={}] - options to apply to the call * @return {Promise} Promise that resolve the api response data or an error. */ post(uri, data = {}, humanKey = null, options = {}) { if (typeof uri === 'string') return this.fetch('POST', uri, data, humanKey, options) return store.dispatch('POST', { ...uri, data, humanKey, options }) }, /** * Api put helper function. * * @param {String|Object} uri * @param {String} [data={}] - data to send as body. * @param {Options} [options={}] - options to apply to the call * @return {Promise} Promise that resolve the api response data or an error. */ put(uri, data = {}, humanKey = null, options = {}) { if (typeof uri === 'string') return this.fetch('PUT', uri, data, humanKey, options) return store.dispatch('PUT', { ...uri, data, humanKey, options }) }, /** * Api delete helper function. * * @param {String|Object} uri * @param {String} [data={}] - data to send as body. * @param {Options} [options={}] - options to apply to the call (default is `{ websocket: false, wait: false }`) * @return {Promise} Promise that resolve the api response data or an error. */ delete(uri, data = {}, humanKey = null, options = {}) { if (typeof uri === 'string') return this.fetch('DELETE', uri, data, humanKey, options) return store.dispatch('DELETE', { ...uri, data, humanKey, options }) }, /** * Api reconnection helper. Resolve when server is reachable or fail after n attemps * * @param {Number} attemps - number of attemps before rejecting * @param {Number} delay - delay between calls to the API in ms. * @param {Number} initialDelay - delay before calling the API for the first time in ms. * @return {Promise} */ tryToReconnect({ attemps = 5, delay = 2000, initialDelay = 0 } = {}) { return new Promise((resolve, reject) => { const api = this function reconnect(n) { api .get('logout', {}, { key: 'reconnecting' }) .then(resolve) .catch((err) => { if (err.name === 'APIUnauthorizedError') { resolve() } else if (n < 1) { reject(err) } else { setTimeout(() => reconnect(n - 1), delay) } }) } if (initialDelay > 0) setTimeout(() => reconnect(attemps), initialDelay) else reconnect(attemps) }) }, }