This repository has been archived on 2024-02-08. You can view files and clone it, but cannot push or open issues or pull requests.
frontend/src/services/abstractService.ts

462 lines
11 KiB
TypeScript
Raw Normal View History

2022-07-20 22:42:36 +00:00
import axios , { type Method } from 'axios'
import {objectToSnakeCase} from '@/helpers/case'
import {getToken} from '@/helpers/auth'
2022-07-20 22:42:36 +00:00
import AbstractModel, { type IAbstract } from '@/models/abstractModel'
2022-07-20 19:15:35 +00:00
import type { Right } from '@/models/constants/rights'
2022-07-20 22:42:36 +00:00
import type { IFile } from '@/models/file'
2022-02-13 20:40:39 +00:00
interface Paths {
create : string
get : string
getAll : string
update : string
delete : string
2022-07-20 22:42:36 +00:00
reset?: string
2022-02-13 20:40:39 +00:00
}
2022-02-13 20:40:39 +00:00
function convertObject(o: Record<string, unknown>) {
if (o instanceof Date) {
return o.toISOString()
}
return o
}
2022-07-20 19:15:35 +00:00
function prepareParams(params: Record<string, unknown | unknown[]>) {
if (typeof params !== 'object') {
return params
}
for (const p in params) {
if (Array.isArray(params[p])) {
params[p] = params[p].map(convertObject)
continue
}
params[p] = convertObject(params[p])
}
return objectToSnakeCase(params)
}
2022-07-20 22:42:36 +00:00
export default class AbstractService<Model extends IAbstract = IAbstract> {
/////////////////////////////
// Initial variable definitions
///////////////////////////
2022-02-13 20:40:39 +00:00
http
loading = false
uploadProgress = 0
2022-02-13 20:40:39 +00:00
paths: Paths = {
create: '',
get: '',
getAll: '',
update: '',
delete: '',
}
2019-12-03 18:09:12 +00:00
// This contains the total number of pages and the number of results for the current page
totalPages = 0
resultCount = 0
/////////////
// Service init
///////////
/**
* The abstract constructor.
2022-02-13 20:40:39 +00:00
* @param [paths] An object with all paths.
*/
2022-02-13 20:40:39 +00:00
constructor(paths : Partial<Paths> = {}) {
this.http = axios.create({
baseURL: window.API_URL,
2022-02-13 20:40:39 +00:00
headers: { 'Content-Type': 'application/json' },
})
2019-06-05 20:15:30 +00:00
// Set the interceptors to process every request
this.http.interceptors.request.use((config) => {
2019-06-05 20:15:30 +00:00
switch (config.method) {
case 'post':
if (this.useUpdateInterceptor()) {
2022-02-18 19:26:55 +00:00
config.data = this.beforeUpdate(config.data)
config.data = objectToSnakeCase(config.data)
}
2019-06-05 20:15:30 +00:00
break
case 'put':
if (this.useCreateInterceptor()) {
2022-02-18 19:26:55 +00:00
config.data = this.beforeCreate(config.data)
config.data = objectToSnakeCase(config.data)
}
2019-06-05 20:15:30 +00:00
break
case 'delete':
if (this.useDeleteInterceptor()) {
2022-02-18 19:26:55 +00:00
config.data = this.beforeDelete(config.data)
config.data = objectToSnakeCase(config.data)
}
2019-06-05 20:15:30 +00:00
break
}
return config
})
// Set the default auth header if we have a token
const token = getToken()
if (token !== null) {
this.http.defaults.headers.common['Authorization'] = `Bearer ${token}`
}
2022-02-13 20:40:39 +00:00
Object.assign(this.paths, paths)
}
2019-11-24 13:16:24 +00:00
/**
* Whether or not to use the create interceptor which processes a request payload into json
*/
2022-02-13 20:40:39 +00:00
useCreateInterceptor(): boolean {
2019-11-24 13:16:24 +00:00
return true
}
/**
* Whether or not to use the update interceptor which processes a request payload into json
*/
2022-02-13 20:40:39 +00:00
useUpdateInterceptor(): boolean {
2019-11-24 13:16:24 +00:00
return true
}
/**
* Whether or not to use the delete interceptor which processes a request payload into json
*/
2022-02-13 20:40:39 +00:00
useDeleteInterceptor(): boolean {
2019-11-24 13:16:24 +00:00
return true
}
2019-12-03 18:09:12 +00:00
/////////////////
// Helper functions
///////////////
/**
* Returns an object with all route parameters and their values.
*/
2022-07-20 19:15:35 +00:00
getRouteReplacements(route : string, parameters : Record<string, unknown> = {}) {
const replace$$1: Record<string, unknown> = {}
let pattern = this.getRouteParameterPattern()
pattern = new RegExp(pattern instanceof RegExp ? pattern.source : pattern, 'g')
for (let parameter; (parameter = pattern.exec(route)) !== null;) {
replace$$1[parameter[0]] = parameters[parameter[1]]
}
return replace$$1
}
/**
* Holds the replacement pattern for url paths, can be overwritten by implementations.
*/
2022-02-13 20:40:39 +00:00
getRouteParameterPattern(): RegExp {
return /{([^}]+)}/
}
/**
* Returns a fully-ready-ready-to-make-a-request-to route with replaced parameters.
*/
2022-02-13 20:40:39 +00:00
getReplacedRoute(path : string, pathparams : {}) : string {
2022-02-15 12:07:34 +00:00
const replacements = this.getRouteReplacements(path, pathparams)
return Object.entries(replacements).reduce(
2022-02-13 20:40:39 +00:00
(result, [parameter, value]) => result.replace(parameter, value as string),
path,
)
}
/**
* setLoading is a method which sets the loading variable to true, after a timeout of 100ms.
* It has the timeout to prevent the loading indicator from showing for only a blink of an eye in the
* case the api returns a response in < 100ms.
* But because the timeout is created using setTimeout, it will still trigger even if the request is
* already finished, so we return a method to call in that case.
*/
2022-07-20 19:15:35 +00:00
setLoading() {
const timeout = setTimeout(() => {
this.loading = true
}, 100)
return () => {
clearTimeout(timeout)
this.loading = false
}
}
//////////////////
// Default factories
// It is possible to specify a factory for each type of request.
// This makes it possible to have different models returned from different routes.
// Specific factories for each request are completly optional, if these are not specified, the defautl factory is used.
////////////////
/**
* The modelFactory returns an model from an object.
* This one here is the default one, usually the service definitions for a model will override this.
*/
2022-02-13 20:40:39 +00:00
modelFactory(data : Partial<Model>) {
return new AbstractModel(data)
}
/**
* This is the model factory for get requests.
*/
2022-02-13 20:40:39 +00:00
modelGetFactory(data : Partial<Model>) {
return this.modelFactory(data)
}
/**
* This is the model factory for get all requests.
*/
2022-02-13 20:40:39 +00:00
modelGetAllFactory(data : Partial<Model>) {
return this.modelFactory(data)
}
/**
* This is the model factory for create requests.
*/
2022-02-13 20:40:39 +00:00
modelCreateFactory(data : Partial<Model>) {
return this.modelFactory(data)
}
/**
* This is the model factory for update requests.
*/
2022-02-13 20:40:39 +00:00
modelUpdateFactory(data : Partial<Model>) {
return this.modelFactory(data)
}
//////////////
// Preprocessors
////////////
/**
* Default preprocessor for get requests
*/
2022-02-13 20:40:39 +00:00
beforeGet(model : Model) {
return model
}
/**
* Default preprocessor for create requests
*/
2022-02-13 20:40:39 +00:00
beforeCreate(model : Model) {
return model
}
/**
* Default preprocessor for update requests
*/
2022-02-13 20:40:39 +00:00
beforeUpdate(model : Model) {
return model
2019-06-05 17:36:32 +00:00
}
/**
* Default preprocessor for delete requests
*/
2022-02-13 20:40:39 +00:00
beforeDelete(model : Model) {
return model
}
///////////////
// Global actions
/////////////
/**
* Performs a get request to the url specified before.
* @param model The model to use. The request path is built using the values from the model.
* @param params Optional query parameters
*/
2022-02-13 20:40:39 +00:00
get(model : Model, params = {}) {
if (this.paths.get === '') {
throw new Error('This model is not able to get data.')
}
return this.getM(this.paths.get, model, params)
}
/**
* This is a more abstract implementation which only does a get request.
* Services which need more flexibility can use this.
*/
2022-07-20 22:42:36 +00:00
async getM(url : string, model : Model = new AbstractModel({}), params: Record<string, unknown> = {}) {
const cancel = this.setLoading()
model = this.beforeGet(model)
const finalUrl = this.getReplacedRoute(url, model)
try {
const response = await this.http.get(finalUrl, {params: prepareParams(params)})
const result = this.modelGetFactory(response.data)
2022-07-20 19:15:35 +00:00
result.maxRight = Number(response.headers['x-max-right']) as Right
return result
} finally {
cancel()
}
}
2022-07-20 22:42:36 +00:00
async getBlobUrl(url : string, method : Method = 'GET', data = {}) {
const response = await this.http({
2022-02-13 20:40:39 +00:00
url,
method,
responseType: 'blob',
2022-02-13 20:40:39 +00:00
data,
})
return window.URL.createObjectURL(new Blob([response.data]))
}
/**
* Performs a get request to the url specified before.
* The difference between this and get() is this one is used to get a bunch of data (an array), not just a single object.
* @param model The model to use. The request path is built using the values from the model.
* @param params Optional query parameters
2019-12-03 18:09:12 +00:00
* @param page The page to get
*/
2022-07-20 22:42:36 +00:00
async getAll(model : Model = new AbstractModel({}), params = {}, page = 1) {
if (this.paths.getAll === '') {
throw new Error('This model is not able to get data.')
}
2019-12-03 18:09:12 +00:00
params.page = page
const cancel = this.setLoading()
model = this.beforeGet(model)
const finalUrl = this.getReplacedRoute(this.paths.getAll, model)
try {
const response = await this.http.get(finalUrl, {params: prepareParams(params)})
this.resultCount = Number(response.headers['x-pagination-result-count'])
this.totalPages = Number(response.headers['x-pagination-total-pages'])
2021-10-17 20:29:05 +00:00
if (response.data === null) {
return []
}
if (Array.isArray(response.data)) {
return response.data.map(entry => this.modelGetAllFactory(entry))
}
return this.modelGetAllFactory(response.data)
} finally {
cancel()
}
}
/**
* Performs a put request to the url specified before
* @returns {Promise<any | never>}
*/
2022-02-13 20:40:39 +00:00
async create(model : Model) {
if (this.paths.create === '') {
throw new Error('This model is not able to create data.')
}
const cancel = this.setLoading()
const finalUrl = this.getReplacedRoute(this.paths.create, model)
try {
const response = await this.http.put(finalUrl, model)
const result = this.modelCreateFactory(response.data)
if (typeof model.maxRight !== 'undefined') {
result.maxRight = model.maxRight
}
return result
} finally {
cancel()
}
}
/**
* An abstract implementation to send post requests.
* Services can use this to implement functions to do post requests other than using the update method.
*/
2022-02-13 20:40:39 +00:00
async post(url : string, model : Model) {
const cancel = this.setLoading()
try {
const response = await this.http.post(url, model)
const result = this.modelUpdateFactory(response.data)
if (typeof model.maxRight !== 'undefined') {
result.maxRight = model.maxRight
}
return result
} finally {
cancel()
}
}
/**
* Performs a post request to the update url
*/
2022-02-13 20:40:39 +00:00
update(model : Model) {
if (this.paths.update === '') {
throw new Error('This model is not able to update data.')
}
const finalUrl = this.getReplacedRoute(this.paths.update, model)
return this.post(finalUrl, model)
}
/**
* Performs a delete request to the update url
*/
2022-02-13 20:40:39 +00:00
async delete(model : Model) {
if (this.paths.delete === '') {
throw new Error('This model is not able to delete data.')
}
const cancel = this.setLoading()
const finalUrl = this.getReplacedRoute(this.paths.delete, model)
try {
const {data} = await this.http.delete(finalUrl, model)
return data
} finally {
cancel()
}
}
/**
* Uploads a file to a url.
* @param url
2022-07-20 22:42:36 +00:00
* @param file {IFile}
* @param fieldName The name of the field the file is uploaded to.
*/
2022-07-20 22:42:36 +00:00
uploadFile(url : string, file: IFile, fieldName : string) {
return this.uploadBlob(url, new Blob([file]), fieldName, file.name)
}
/**
* Uploads a blob to a url.
*/
2022-02-13 20:40:39 +00:00
uploadBlob(url : string, blob: Blob, fieldName: string, filename : string) {
const data = new FormData()
data.append(fieldName, blob, filename)
return this.uploadFormData(url, data)
}
/**
* Uploads a form data object.
*/
2022-07-20 19:15:35 +00:00
async uploadFormData(url : string, formData: FormData) {
const cancel = this.setLoading()
try {
const response = await this.http.put(
url,
formData,
{
headers: {
'Content-Type':
2021-10-17 20:29:05 +00:00
'multipart/form-data; boundary=' + formData._boundary,
},
onUploadProgress: progressEvent => {
this.uploadProgress = Math.round((progressEvent.loaded * 100) / progressEvent.total)
},
},
)
2021-10-17 20:29:05 +00:00
return this.modelCreateFactory(response.data)
} finally {
this.uploadProgress = 0
cancel()
}
}
}