/**
* @namespace api
* @category api
* @module api-wrapper
* */
import Vue from 'vue'
import axios from 'axios'
import qs from 'qs'
import fixtures from './fixtures'
import { getQueryStringValue } from '@/utils/querystring'
import { mitt } from '@/plugins/mitt.js'
import { isUnitTest } from '@/utils/unit.js'
import { useApiPlatformResources } from './api-platform-resources.js'
import {
getCachedJWT,
setCacheFromAPIResponse,
getCacheFromAPIRequestURL,
} from './api-cache.js'
import { errorLogger } from '@/plugins/error-logger.js'
import { getNestedValue } from '../utils/object'
import APIUrls from '@/config/simpliciti-apis.js'
const shouldLog = isUnitTest()
? true
: (getQueryStringValue('verbose') || '').includes('1') ||
(getQueryStringValue('verbose') || '').includes('api')
const bus = {
$on: (name, callback) => {
return mitt.on(name, callback)
},
$emit: (name, payload) => {
return mitt.emit(name, payload)
},
}
const globalOptions = {
headers: {
accept: 'application/json',
},
}
const defaultGetCacheFromAPIRequestURLOptions = {
beforeCacheFetch(url) {
//Location module: Skip cache if requesting data from today.
if (url.includes(require('moment')().format('YYYY-MM-DD'))) {
shouldLog &&
console.log('beforeCacheFetch::skip-history-request-if-today')
return false
}
},
}
/**
*
* @param {*} defaultOptions
* @returns {Object}
*/
function getApi(defaultOptions = {}) {
async function getRequestOptions(url, options = {}, data = null) {
options = {
...globalOptions,
...defaultOptions,
...options,
}
let isPublicURL = !!url && url.includes('public')
let jwt = null
if (!isPublicURL && options.requestHandler === undefined) {
jwt = options.jwt || options._token || (await getCachedJWT(url))
}
options.headers = {
...(options.headers || {}),
Authorization: 'Bearer ' + jwt,
}
options.url += ('/' + url).split('//').join('/') //Concatenate with '/' if not present
options.relativeUrl = url
if (data) {
options.data = data
}
return options
}
return {
APIUrls,
url: defaultOptions.url || globalOptions.url || '',
bus,
get: async function (url, data = {}, options = {}) {
//Build full url before checking cache
if (Object.keys(data).length > 0) {
url =
url +
(url.indexOf('?') !== -1 ? '&' : '?') +
Object.keys(data)
.map((key) => {
if (data[key] instanceof Array) {
return data[key]
.map((item) => {
return `${key}[]=${item}`
})
.join('&')
} else {
return `${key}=${data[key]}`
}
})
.join('&')
}
let cachedResponse = await getCacheFromAPIRequestURL(
url,
defaultGetCacheFromAPIRequestURLOptions,
options
)
shouldLog &&
console.log('api.get.request', {
url,
cached: !!cachedResponse,
})
if (cachedResponse) {
return cachedResponse
}
let getOptions = {
method: 'get',
...(await getRequestOptions(url, options)),
}
shouldLog && console.info('get', url)
return await processAxios(
createAxiosRequest(getOptions, options),
getOptions,
options,
this
)
},
post: async function (url, data = {}, options = {}) {
let cachedResponse = await getCacheFromAPIRequestURL(
url,
defaultGetCacheFromAPIRequestURLOptions,
options
)
if (cachedResponse) {
return cachedResponse
}
let postOptions = {
method: 'post',
...(await getRequestOptions(url, options, data)),
}
shouldLog &&
console.log('api.post.request', {
url: postOptions.url,
})
return await processAxios(axios(postOptions), postOptions, options)
},
delete: async function (url, data = {}, options = {}) {
let requestOptions = {
method: 'delete',
...(await getRequestOptions(url, options, data)),
}
shouldLog && console.info('delete', url)
return await processAxios(axios(requestOptions), requestOptions, options)
},
patch: async function (url, data = {}, options = {}) {
let requestOptions = {
method: 'patch',
...(await getRequestOptions(url, options, data)),
}
shouldLog && console.info('patch', url)
requestOptions.headers['Content-type'] = 'application/merge-patch+json'
return await processAxios(axios(requestOptions), requestOptions, options)
},
postFormUrlEncoded: async function (url, data = {}, options = {}) {
let cachedResponse = await getCacheFromAPIRequestURL(
url,
defaultGetCacheFromAPIRequestURLOptions,
options
)
if (cachedResponse) {
return cachedResponse
}
let postOptions = {
method: 'post',
...(await getRequestOptions(url, options, qs.stringify(data))), //@warn: Why are we passing querystring values?
}
postOptions.headers['Content-type'] = 'application/x-www-form-urlencoded'
postOptions.headers.accept = 'application/json'
shouldLog && console.info('postFormUrlEncoded', url, postOptions, options)
return await processAxios(axios(postOptions), postOptions, options)
},
}
}
/**
* Unit tests can fake axios request using api.get(url, {requestHandler:()=>{}})
* @param {Object} requestOptions computed request options
* @param {Function} options.requestHandler Handler to fake axios request
* @returns
*/
function createAxiosRequest(requestOptions, options = {}) {
//console.log('createAxiosRequest', options)
if (options.requestHandler) {
let promiseOrNot = options.requestHandler(requestOptions)
if (!(promiseOrNot instanceof Promise)) {
return new Promise((resolve) => {
resolve(promiseOrNot)
})
} else {
return promiseOrNot
}
} else {
return axios(requestOptions)
}
}
const api = {
loadFixtures: (identifier, transform) =>
fixtures.loadFixtures(identifier, transform),
toggleFixtures: (value) => fixtures.toggleFixtures(value),
areFixturesEnabled: {
get() {
return fixtures.areFixturesEnabled
},
},
...getApi({
...globalOptions,
url: process.env.VUE_APP_API_GEORED_HOST,
}),
getApi,
v2: getApi({
...globalOptions,
url: process.env.VUE_APP_API_GEORED_HOST,
}),
v3: getApi({
...globalOptions,
withCredentials: false,
url: process.env.VUE_APP_API_GEORED_V3_HOST,
}),
}
Object.assign(
api,
useApiPlatformResources({
apiWrapper: api,
getAPIV3Pooling,
})
)
/**
* @deprecated Vue3-compatibility: Import module instead: import api from '@/api.js'
*/
Vue.use({
install() {
Vue.$api = Vue.prototype.$api = api
},
})
export default api
function promptForDownload(
axiosResponse,
defaultFileName = 'filename',
forceDefaultDownloaFilename = false
) {
const disposition = axiosResponse.headers['content-disposition']
const match = disposition && disposition.match(/filename=([^;]+)/)
const filename = forceDefaultDownloaFilename
? defaultFileName
: (match && match[1].trim()) || defaultFileName
const url = window.URL.createObjectURL(new Blob([axiosResponse.data]))
const link = document.createElement('a')
link.href = url
link.setAttribute('download', filename)
document.body.appendChild(link)
link.click()
}
async function processAxios(
axiosPromise,
requestOptions,
operationOptions = {},
methods = null
) {
let res = {}
try {
res = await axiosPromise
if (!res) {
throw new Error('invalid_response')
}
} catch (err) {
res = err
if (!res) {
res = new Error('unknown_error')
}
}
if (res instanceof Error) {
;[401, 403, 500].forEach((errorCode) => {
if (res.message.indexOf(errorCode.toString()) !== -1) {
res.status = errorCode
return false
}
})
}
if (res.status && !['200', '201'].includes(res.status.toString())) {
console.warn('API Response', {
status: res.status,
//requestOptions,
response: res.data || res,
})
bus.$emit(res.status.toString(), res)
}
if (res instanceof Error) {
console.warn('api-wrapper::server-error', res.stack)
throw res
} else {
if ([401, 403, 500].includes(res.status)) {
shouldLog && console.log('api-wrapper::server-error(custom)')
throw new Error(res.status)
}
}
if (operationOptions.isDownload) {
promptForDownload(
res,
operationOptions.defaultDownloadFilename || '',
operationOptions.forceDefaultDownloaFilename
)
}
let responseBody = res.data || {}
let apiHeaders = {}
Object.keys(responseBody)
.filter((k) => k !== 'data')
.forEach((k) => (apiHeaders[k] = responseBody[k]))
res.apiHeaders = apiHeaders
//Server errors are output to console in any env
try {
if (responseBody.error) {
console.error(
`api-wrapper::server-error: ${JSON.stringify(responseBody.error)}`
)
}
} catch (err) {
console.log('api-wrapper::server-error')
console.error(responseBody.error)
}
if (responseBody.data) {
res.data = responseBody.data
}
let keys = Object.keys(res.data)
res.isArray = res.data instanceof Array
if (typeof res.data.items !== 'undefined') {
res.isArray = res.data.items instanceof Array
res.data = res.data.items || []
}
if (!res.isArray) {
res.data = res.data || {}
}
if (res?.data?.error instanceof Array && res?.data?.error.length > 0) {
console.warn('api-wrapper::server-error', res)
if ((res.data.error[0].message || '').includes("Vous n'avez accès")) {
bus.$emit('403')
res.data = []
}
}
try {
delete requestOptions.headers
} catch (err) {
//
}
shouldLog &&
console.log(
'api_response',
//`request:`,
//requestOptions,
`url`,
requestOptions.url,
res.data.length ? `(${res.data.length} items)` : '(object)',
{
data:
res.data instanceof Array
? (res.data.length > 0 && `Keys: ${Object.keys(res.data[0])}`) || []
: `Keys are ` + keys.join(', '),
}
)
if (operationOptions.populate && res.data instanceof Array && methods) {
await populateApiPlatformResponse(res, operationOptions.populate, methods)
}
try {
await setCacheFromAPIResponse(
requestOptions.relativeUrl,
res,
operationOptions
)
} catch (err) {
console.warn('API-WRAPPER: Fail to cache response')
errorLogger.logError(err)
}
return res
}
/**
* @warn unused feature
* Used by Diagnostic module to populate sensor assigments with sensors details (i.g To Match ANA1 to ANA1 sensor assigments item)
* @param {Object} res API Wrapper response {data:[]}
* @param {Array} populateOptions i.g ['associatedSensor'] or {items:['associatedSensor']}
*/
export async function populateApiPlatformResponse(
res,
populateOptions,
methods
) {
let populateItems = []
if (populateOptions instanceof Array && populateOptions.items === undefined) {
populateItems = populateOptions
} else {
populateItems = populateOptions.items
}
let dataArray = populateOptions.arrayHandler
? populateOptions.arrayHandler(res.data)
: res.data
console.log('api::populate', {
populateItems,
dataArray,
populateOptions,
})
for (let populateItem of populateItems) {
for (
let resItemIndex = 0;
resItemIndex < dataArray.length;
resItemIndex++
) {
let resItem = dataArray[resItemIndex]
if (resItem[populateItem] && resItem[populateItem].charAt(0) === '/') {
console.log('api::populate', populateItem)
if (populateOptions.populateHandler) {
dataArray[resItemIndex][populateItem] =
populateOptions.populateHandler(resItem[populateItem])
if (dataArray[resItemIndex][populateItem] instanceof Promise) {
dataArray[resItemIndex][populateItem] = await dataArray[
resItemIndex
][populateItem]
}
} else {
dataArray[resItemIndex][populateItem] = (
await methods.get(resItem[populateItem])
).data
}
console.log(
'api::populate::success',
dataArray[resItemIndex][populateItem]
)
} else {
console.log('api::populate::skip', resItem)
}
}
}
}
/**
*
* @param {String} options.uri i.g /geored/client/hierarchies?page=1&itemsPerPage=100
* @param {Object} options.payload Used for querystring parameters {query:'foo'} will add ?query=foo
* @param {Boolean} options.forcePayload Payload will be also used in pagination calls (Intermediate requests)
* @param {function} options.callback To be called for each response
* @returns
*/
export function getAPIV3Pooling(options = {}) {
let callback =
options.callback ||
((data) => {
//console.log(`getAPIV3Pooling::callback`, { data })
})
return new Promise((resolve, reject) => {
let items = []
async function getAPIV3PoolingHandler(uri, isPagination = false) {
let querystringPayload = isPagination ? {} : options.payload || {}
if (options.forcePayload) {
querystringPayload = {
...querystringPayload,
...options.payload,
}
}
let data = (
await api.v3.get(uri, querystringPayload, {
headers: {
Accept: 'application/hal+json',
},
...(options.fetchOptions || {}),
})
).data
let newItems = getNestedValue(data, '_embedded.item', [])
if (options.transform) {
newItems = newItems.map(options.transform)
}
items = items.concat(newItems)
let shouldAbort = callback(newItems) === false
let isLastPage =
!data?._links?.next?.href ||
(data?._links?.self?.href || '') == (data?._links?.last?.href || '')
shouldLog &&
console.log('getAPIV3Pooling', {
isLastPage,
shouldAbort,
})
if (isLastPage || shouldAbort) {
shouldLog &&
console.log('getAPIV3Pooling:resolved', {
items,
})
resolve(items)
} else {
getAPIV3PoolingHandler(data._links.next.href, true).catch(reject)
}
}
getAPIV3PoolingHandler(options.uri).catch(reject)
})
}
Source