mirror of
https://github.com/YunoHost/yunohost-admin.git
synced 2024-09-03 20:06:15 +02:00
Rework the whole API calling system
This commit is contained in:
parent
4d21966fc7
commit
b130aeda29
4 changed files with 157 additions and 89 deletions
|
@ -4,14 +4,33 @@
|
|||
*/
|
||||
|
||||
import store from '@/store'
|
||||
import { handleResponse } from './handlers'
|
||||
import { openWebSocket, getResponseData, handleError } from './handlers'
|
||||
import { objectToParams } from '@/helpers/commons'
|
||||
|
||||
|
||||
/**
|
||||
* A digested fetch response as an object, a string or an error.
|
||||
* @typedef {(Object|string|Error)} DigestedResponse
|
||||
* 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} noCache - if `true`, will disable the cache mecanism for this call.
|
||||
* @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"
|
||||
*/
|
||||
|
||||
|
||||
export default {
|
||||
options: {
|
||||
credentials: 'include',
|
||||
|
@ -22,111 +41,124 @@ export default {
|
|||
// Auto header is :
|
||||
// "Accept": "*/*",
|
||||
|
||||
// Also is this still important ? (needed by back-end)
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Opens a WebSocket connection to the server in case it sends messages.
|
||||
* Currently, the connection is closed by the server right after an API call so
|
||||
* we have to open it for every calls.
|
||||
* Messages are dispatch to the store so it can handle them.
|
||||
*
|
||||
* @return {Promise<Event>} Promise that resolve on websocket 'open' or 'error' event.
|
||||
*/
|
||||
openWebSocket () {
|
||||
return new Promise(resolve => {
|
||||
const ws = new WebSocket(`wss://${store.getters.host}/yunohost/api/messages`)
|
||||
ws.onmessage = ({ data }) => store.dispatch('DISPATCH_MESSAGE', JSON.parse(data))
|
||||
// ws.onclose = (e) => {}
|
||||
ws.onopen = resolve
|
||||
// Resolve also on error so the actual fetch may be called.
|
||||
ws.onerror = resolve
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 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 for 'POST', 'PUT' and 'DELETE' methods.
|
||||
* @param {Object} [options={}]
|
||||
* @param {Boolean} [options.websocket=true] - Open a websocket before this request.
|
||||
* @return {Promise<Response>} Promise that resolve a fetch `Response`.
|
||||
* @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<Object|Error>} Promise that resolve the api response data or an error.
|
||||
*/
|
||||
async fetch (method, uri, data = {}, { websocket = true } = {}) {
|
||||
// Open a websocket connection that will dispatch messages received.
|
||||
async fetch (method, uri, data = {}, { 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, initial, wait, websocket })
|
||||
|
||||
if (websocket) {
|
||||
await this.openWebSocket()
|
||||
store.dispatch('WAITING_FOR_RESPONSE', [uri, method])
|
||||
await openWebSocket(request)
|
||||
}
|
||||
|
||||
let options = this.options
|
||||
if (method === 'GET') {
|
||||
const localeQs = `${uri.includes('?') ? '&' : '?'}locale=${store.getters.locale}`
|
||||
return fetch('/yunohost/api/' + uri + localeQs, this.options)
|
||||
uri += `${uri.includes('?') ? '&' : '?'}locale=${store.getters.locale}`
|
||||
} else {
|
||||
options = { ...options, method, body: objectToParams(data, { addLocale: true }) }
|
||||
}
|
||||
|
||||
return fetch('/yunohost/api/' + uri, {
|
||||
...this.options,
|
||||
method,
|
||||
body: objectToParams(data, { addLocale: 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<Query>} queries - An array of queries with special representation.
|
||||
* @param {Object} [options={}]
|
||||
* @param {Boolean}
|
||||
* @return {Promise<Array|Error>} 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, options = {}] of queries) {
|
||||
if (wait) options.wait = false
|
||||
if (initial) options.initial = true
|
||||
results.push(await this[method.toLowerCase()](uri, data, 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} uri - the uri to call.
|
||||
* @param {Object} [options={}]
|
||||
* @param {Boolean} [options.websocket=false] - Open a websocket before this request.
|
||||
* @return {Promise<module:api~DigestedResponse>} Promise that resolve the api response as an object, a string or as an error.
|
||||
* @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<Object|Error>} Promise that resolve the api response data or an error.
|
||||
*/
|
||||
get (uri, { websocket = false } = {}) {
|
||||
return this.fetch('GET', uri, null, { websocket }).then(response => handleResponse(response, 'GET'))
|
||||
get (uri, data = null, options = {}) {
|
||||
options = { websocket: false, wait: false, ...options }
|
||||
if (typeof uri === 'string') return this.fetch('GET', uri, null, options)
|
||||
return store.dispatch('GET', { ...uri, options })
|
||||
},
|
||||
|
||||
/**
|
||||
* Api get helper function for multiple queries.
|
||||
*
|
||||
* @param {string} uri - the uri to call.
|
||||
* @return {Promise<module:api~DigestedResponse[]>} Promise that resolve the api responses as an array.
|
||||
*/
|
||||
getAll (uris) {
|
||||
return Promise.all(uris.map(uri => this.get(uri)))
|
||||
},
|
||||
|
||||
/**
|
||||
* Api post helper function.
|
||||
*
|
||||
* @param {string} uri - the uri to call.
|
||||
* @param {string} [data={}] - data to send as body.
|
||||
* @return {Promise<module:api~DigestedResponse>} Promise that resolve the api responses as an array.
|
||||
* @param {String|Object} uri
|
||||
* @param {String} [data={}] - data to send as body.
|
||||
* @param {Options} [options={}] - options to apply to the call
|
||||
* @return {Promise<Object|Error>} Promise that resolve the api response data or an error.
|
||||
*/
|
||||
post (uri, data = {}) {
|
||||
return this.fetch('POST', uri, data).then(response => handleResponse(response, 'POST'))
|
||||
post (uri, data = {}, options = {}) {
|
||||
if (typeof uri === 'string') return this.fetch('POST', uri, data, options)
|
||||
return store.dispatch('POST', { ...uri, data, options })
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Api put helper function.
|
||||
*
|
||||
* @param {string} uri - the uri to call.
|
||||
* @param {string} [data={}] - data to send as body.
|
||||
* @return {Promise<module:api~DigestedResponse>} Promise that resolve the api responses as an array.
|
||||
* @param {String|Object} uri
|
||||
* @param {String} [data={}] - data to send as body.
|
||||
* @param {Options} [options={}] - options to apply to the call
|
||||
* @return {Promise<Object|Error>} Promise that resolve the api response data or an error.
|
||||
*/
|
||||
put (uri, data = {}) {
|
||||
return this.fetch('PUT', uri, data).then(response => handleResponse(response, 'PUT'))
|
||||
put (uri, data = {}, options = {}) {
|
||||
if (typeof uri === 'string') return this.fetch('PUT', uri, data, options)
|
||||
return store.dispatch('PUT', { ...uri, data, options })
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Api delete helper function.
|
||||
*
|
||||
* @param {string} uri - the uri to call.
|
||||
* @param {string} [data={}] - data to send as body.
|
||||
* @return {Promise<('ok'|Error)>} Promise that resolve the api responses as an array.
|
||||
* @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<Object|Error>} Promise that resolve the api response data or an error.
|
||||
*/
|
||||
delete (uri, data = {}) {
|
||||
return this.fetch('DELETE', uri, data).then(response => handleResponse(response, 'DELETE'))
|
||||
delete (uri, data = {}, options = {}) {
|
||||
if (typeof uri === 'string') return this.fetch('DELETE', uri, data, options)
|
||||
return store.dispatch('DELETE', { ...uri, data, options })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,15 +7,15 @@ import i18n from '@/i18n'
|
|||
|
||||
|
||||
class APIError extends Error {
|
||||
constructor (method, { url, status, statusText }, errorData) {
|
||||
constructor (request, { url, status, statusText }, errorData) {
|
||||
super(errorData.error || i18n.t('error_server_unexpected'))
|
||||
const urlObj = new URL(url)
|
||||
this.name = 'APIError'
|
||||
this.code = status
|
||||
this.status = statusText
|
||||
this.method = method
|
||||
this.method = request.method
|
||||
this.request = request
|
||||
this.path = urlObj.pathname + urlObj.search
|
||||
this.logRef = errorData.log_ref || null
|
||||
}
|
||||
|
||||
log () {
|
||||
|
@ -23,6 +23,15 @@ class APIError extends Error {
|
|||
}
|
||||
}
|
||||
|
||||
// Log (Special error to trigger a redirect to a log page)
|
||||
class APIErrorLog extends APIError {
|
||||
constructor (method, response, errorData) {
|
||||
super(method, response, errorData)
|
||||
this.logRef = errorData.log_ref
|
||||
this.name = 'APIErrorLog'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 0 — (means "the connexion has been closed" apparently)
|
||||
class APIConnexionError extends APIError {
|
||||
|
@ -83,6 +92,7 @@ class APINotRespondingError extends APIError {
|
|||
// Temp factory
|
||||
const errors = {
|
||||
[undefined]: APIError,
|
||||
log: APIErrorLog,
|
||||
0: APIConnexionError,
|
||||
400: APIBadRequestError,
|
||||
401: APIUnauthorizedError,
|
||||
|
@ -95,6 +105,7 @@ const errors = {
|
|||
export {
|
||||
errors as default,
|
||||
APIError,
|
||||
APIErrorLog,
|
||||
APIBadRequestError,
|
||||
APIConnexionError,
|
||||
APIInternalError,
|
||||
|
|
|
@ -13,7 +13,7 @@ import errors, { APIError } from './errors'
|
|||
* @param {Response} response - A fetch `Response` object.
|
||||
* @return {(Object|String)} Parsed response's json or response's text.
|
||||
*/
|
||||
async function _getResponseData (response) {
|
||||
export async function getResponseData (response) {
|
||||
// FIXME the api should always return json as response
|
||||
const responseText = await response.text()
|
||||
try {
|
||||
|
@ -25,36 +25,58 @@ async function _getResponseData (response) {
|
|||
|
||||
|
||||
/**
|
||||
* Handler for API responses.
|
||||
* Opens a WebSocket connection to the server in case it sends messages.
|
||||
* Currently, the connection is closed by the server right after an API call so
|
||||
* we have to open it for every calls.
|
||||
* Messages are dispatch to the store so it can handle them.
|
||||
*
|
||||
* @param {Response} response - A fetch `Response` object.
|
||||
* @return {(Object|String)} Parsed response's json, response's text or an error.
|
||||
* @param {Object} request - Request info data.
|
||||
* @return {Promise<Event>} Promise that resolve on websocket 'open' or 'error' event.
|
||||
*/
|
||||
export async function handleResponse (response, method) {
|
||||
const responseData = await _getResponseData(response)
|
||||
store.dispatch('SERVER_RESPONDED', response.ok)
|
||||
return response.ok ? responseData : handleError(response, responseData, method)
|
||||
export function openWebSocket (request) {
|
||||
return new Promise(resolve => {
|
||||
const ws = new WebSocket(`wss://${store.getters.host}/yunohost/api/messages`)
|
||||
ws.onmessage = ({ data }) => {
|
||||
store.dispatch('DISPATCH_MESSAGE', { request, messages: JSON.parse(data) })
|
||||
}
|
||||
// ws.onclose = (e) => {}
|
||||
ws.onopen = resolve
|
||||
// Resolve also on error so the actual fetch may be called.
|
||||
ws.onerror = resolve
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for API errors.
|
||||
*
|
||||
* @param {Response} response - A fetch `Response` object.
|
||||
* @throws Will throw a custom error with response data.
|
||||
* @param {Object} request - Request info data.
|
||||
* @param {Response} response - A consumed fetch `Response` object.
|
||||
* @param {Object|String} errorData - The response parsed json/text.
|
||||
* @throws Will throw a `APIError` with request and response data.
|
||||
*/
|
||||
export async function handleError (response, errorData, method) {
|
||||
const errorCode = response.status in errors ? response.status : undefined
|
||||
// FIXME API: Patching errors that are plain text or html.
|
||||
export async function handleError (request, response, errorData) {
|
||||
let errorCode = response.status in errors ? response.status : undefined
|
||||
if (typeof errorData === 'string') {
|
||||
// FIXME API: Patching errors that are plain text or html.
|
||||
errorData = { error: errorData }
|
||||
}
|
||||
if ('log_ref' in errorData) {
|
||||
// Define a special error so it won't get caught as a `APIBadRequestError`.
|
||||
errorCode = 'log'
|
||||
}
|
||||
|
||||
// This error can be catched by a view otherwise it will be catched by the `onUnhandledAPIError` handler.
|
||||
throw new errors[errorCode](method, response, errorData)
|
||||
throw new errors[errorCode](request, response, errorData)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* If an APIError is not catched by a view it will be dispatched to the store so the
|
||||
* error can be displayed in the error modal.
|
||||
*
|
||||
* @param {APIError} error
|
||||
*/
|
||||
export function onUnhandledAPIError (error) {
|
||||
// In 'development', Babel seems to also catch the error so there's no need to log it twice.
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
|
@ -64,9 +86,12 @@ export function onUnhandledAPIError (error) {
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* Global catching of unhandled promise's rejections.
|
||||
* Those errors (thrown or rejected from inside a promise) can't be catched by
|
||||
* `window.onerror`.
|
||||
*/
|
||||
export function registerGlobalErrorHandlers () {
|
||||
// Global catching of unhandled promise's rejections.
|
||||
// Those errors (thrown or rejected from inside a promise) can't be catched by `window.onerror`.
|
||||
window.addEventListener('unhandledrejection', e => {
|
||||
const error = e.reason
|
||||
if (error instanceof APIError) {
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
export { default } from './api'
|
||||
export { handleResponse, handleError, registerGlobalErrorHandlers } from './handlers'
|
||||
export { handleError, registerGlobalErrorHandlers } from './handlers'
|
||||
|
|
Loading…
Reference in a new issue