diff --git a/src/bootstrap.js b/src/bootstrap.js index f533f24..90da528 100644 --- a/src/bootstrap.js +++ b/src/bootstrap.js @@ -1,12 +1,13 @@ -import ApiService from '@/services/api.service'; +import ApiService from '@/repositories/api.repository'; + import TokenService from '@/services/token.service'; import ThemeService from '@/services/theme.service'; import LanguageService from '@/services/language.service'; -import AuthenticateUser from '@/services/middleware/AuthenticateUser'; +import AuthenticateUser from '@/repositories/middleware/AuthenticateUser'; -import HttpHeader from '@/enums/HttpHeader'; import MimeType from '@/enums/MimeType'; +import HttpHeader from '@/enums/HttpHeader'; ApiService.setHeader(HttpHeader.CONTENT_TYPE, MimeType.APPLICATION_JSON); ApiService.addResponseMiddleware(AuthenticateUser); diff --git a/src/composables/todo.composable.js b/src/composables/todo.composable.js index d4fbcff..96ff09b 100644 --- a/src/composables/todo.composable.js +++ b/src/composables/todo.composable.js @@ -1,7 +1,7 @@ import { ref, computed } from 'vue'; // Service -import TodoService from '@/services/todo.service'; +import TodoRepository from '@/repositories/todo.repository'; // Composables import { useLoading } from '@/composables/loading.composable'; @@ -22,7 +22,7 @@ export function useFetchTodos() { function fetchTodos(config) { startLoading(); - return TodoService.getAll(config) + return TodoRepository.getAll(config) .then(function (response) { todos.value = response.data; return response; @@ -40,7 +40,7 @@ export function useFetchTodos() { }; } -export function useFetchTodo(initialValue = TodoService.getDefault()) { +export function useFetchTodo(initialValue = TodoRepository.getDefault()) { const { isLoading, startLoading, endLoading } = useLoading(); const todo = ref(initialValue); @@ -48,7 +48,7 @@ export function useFetchTodo(initialValue = TodoService.getDefault()) { function fetchTodoById(id) { startLoading(); - return TodoService.getOneById(id) + return TodoRepository.getOneById(id) .then(function (response) { todo.value = response.data; return response; diff --git a/src/composables/user.composable.js b/src/composables/user.composable.js index 309f1df..cef6e73 100644 --- a/src/composables/user.composable.js +++ b/src/composables/user.composable.js @@ -1,7 +1,7 @@ import { ref, computed } from 'vue'; // Service -import UserService from '@/services/user.service'; +import UserRepository from '@/repositories/user.repository'; // Composables import { useLoading } from '@/composables/loading.composable'; @@ -19,7 +19,7 @@ export function useFetchUsers() { function fetchUsers() { startLoading(); - return UserService.getAll() + return UserRepository.getAll() .then(function (response) { users.value = response.data; return response; @@ -37,7 +37,7 @@ export function useFetchUsers() { }; } -export function useFetchUser(initialValue = UserService.getDefault()) { +export function useFetchUser(initialValue = UserRepository.getDefault()) { const { isLoading, startLoading, endLoading } = useLoading(); const user = ref(initialValue); @@ -45,7 +45,7 @@ export function useFetchUser(initialValue = UserService.getDefault()) { function fetchUserById(id) { startLoading(); - return UserService.getOneById(id) + return UserRepository.getOneById(id) .then(function (response) { user.value = response.data; return response; diff --git a/src/services/api.service.js b/src/repositories/api.repository.js similarity index 98% rename from src/services/api.service.js rename to src/repositories/api.repository.js index 047b07a..b2cc8d7 100644 --- a/src/services/api.service.js +++ b/src/repositories/api.repository.js @@ -17,7 +17,7 @@ const instance = axios.create({ timeout: import.meta.env.VITE_API_TIMEOUT }); -class ApiService { +class ApiRepository { /** * Set header for all or specific http method * @@ -142,4 +142,4 @@ class ApiService { } } -export default ApiService; +export default ApiRepository; diff --git a/src/repositories/base.repository.js b/src/repositories/base.repository.js new file mode 100644 index 0000000..e257ba1 --- /dev/null +++ b/src/repositories/base.repository.js @@ -0,0 +1,15 @@ +class BaseRepository { + /** + * Repository url + * + * @throws {Error} + * @returns {String} + */ + get URL() { + throw new Error( + 'You have to implement the static method "URL", for each class that extend BaseRepositories!' + ); + } +} + +export default BaseRepository; \ No newline at end of file diff --git a/src/services/crud.service.js b/src/repositories/crud.repository.js similarity index 50% rename from src/services/crud.service.js rename to src/repositories/crud.repository.js index d3e062b..e013cd8 100644 --- a/src/services/crud.service.js +++ b/src/repositories/crud.repository.js @@ -1,26 +1,15 @@ -import ApiService from './api.service'; - -class CrudService { - /** - * Service url - * - * @throws {Error} - * @returns {String} - */ - static get URL() { - throw new Error( - 'You have to implement the static method "URL", for each class that extend CrudServices!' - ); - } +import ApiRepository from './api.repository'; +import BaseRepository from './base.repository'; +class CrudRepository extends BaseRepository { /** * Get items * * @param {AxiosRequestConfig} [config] * @returns {Promise} */ - static getAll(config) { - return ApiService.get(this.URL, config); + getAll(config) { + return ApiRepository.get(this.URL, config); } /** @@ -30,8 +19,8 @@ class CrudService { * @param {AxiosRequestConfig} [config] * @returns {Promise} */ - static getOneById(id, config) { - return ApiService.get(`${this.URL}/${id}`, config); + getOneById(id, config) { + return ApiRepository.get(`${this.URL}/${id}`, config); } /** @@ -41,8 +30,8 @@ class CrudService { * @param {AxiosRequestConfig} [config] * @returns {Promise} */ - static create(data, config) { - return ApiService.post(`${this.URL}/create`, data, config); + create(data, config) { + return ApiRepository.post(`${this.URL}/create`, data, config); } /** @@ -53,8 +42,8 @@ class CrudService { * @param {AxiosRequestConfig} [config] * @returns {Promise} */ - static update(id, data, config) { - return ApiService.post(`${this.URL}/${id}/update`, data, config); + update(id, data, config) { + return ApiRepository.post(`${this.URL}/${id}/update`, data, config); } /** @@ -65,9 +54,9 @@ class CrudService { * @param {AxiosRequestConfig} [config] * @returns {Promise} */ - static delete(id, data, config) { - return ApiService.post(`${this.URL}/${id}/delete`, data, config); + delete(id, data, config) { + return ApiRepository.post(`${this.URL}/${id}/delete`, data, config); } } -export default CrudService; +export default CrudRepository; \ No newline at end of file diff --git a/src/services/middleware/AuthenticateUser.js b/src/repositories/middleware/AuthenticateUser.js similarity index 100% rename from src/services/middleware/AuthenticateUser.js rename to src/repositories/middleware/AuthenticateUser.js index e88d99b..440eb59 100644 --- a/src/services/middleware/AuthenticateUser.js +++ b/src/repositories/middleware/AuthenticateUser.js @@ -1,6 +1,6 @@ import AuthenticationService from '@/services/authentication.service'; -import router from '@/router/index'; +import router from '@/router/index'; import HttpStatusCode from '@/enums/HttpStatusCode'; /** diff --git a/src/repositories/strategy/BaseStrategy.js b/src/repositories/strategy/BaseStrategy.js new file mode 100644 index 0000000..e4f8bf3 --- /dev/null +++ b/src/repositories/strategy/BaseStrategy.js @@ -0,0 +1,96 @@ +// Utils +import { isEmptyObject } from '@/utils'; + +// Services +import LocalStorageService from '@/services/local-storage.service'; + +const Default = { + ttl: 60_000, + driver: LocalStorageService, + cacheTag: 'global' +}; + +class BaseStrategy { + /** + * @type {Object} + * @private + */ + _cache; + + /** + * @type {Number} + * @private + */ + _ttl; + + /** + * @type {Object} + * @private + */ + _driver; + + /** + * @type {String} + * @private + */ + _cacheTag; + + /** + * @param {Object} config + * @param {Number} [config.ttl] + * @param {Object} [config.driver] + * @param {String} [config.cacheTag] + */ + constructor(config) { + // Todo => checking the type of configurations + const { ttl, driver, cacheTag } = { + ...Default, + ...(typeof config === 'object' ? config : {}) + }; + + this._ttl = ttl; + this._driver = driver; + this._cacheTag = cacheTag; + + this._cache = this._initializeCache(); + } + + // Private + + /** + * Get cache bu tag + * + * @private + * @returns {*|{}} + */ + _initializeCache() { + return this._driver.get(this._cacheTag) ?? {}; + } + + /** + * Save cache by tag + * + * @private + * @returns void + */ + _saveCache() { + if (isEmptyObject(this._cache)) { + this._clearCache(); + return; + } + + this._driver.set(this._cacheTag, this._cache); + } + + /** + * Clear cache by tag + * + * @private + * @returns void + */ + _clearCache() { + this._driver.delete(this._cacheTag); + } +} + +export default BaseStrategy; \ No newline at end of file diff --git a/src/repositories/strategy/CacheFirstStrategy.js b/src/repositories/strategy/CacheFirstStrategy.js new file mode 100644 index 0000000..e5705d2 --- /dev/null +++ b/src/repositories/strategy/CacheFirstStrategy.js @@ -0,0 +1,97 @@ +import BaseStrategy from './BaseStrategy'; + +class CacheFirstStrategy extends BaseStrategy { + /** + * @param {Object} config + * @param {Number} [config.ttl] + * @param {Object} [config.driver] + * @param {String} [config.cacheTag] + */ + constructor(config) { + super(config); + } + + /** + * Store a value by cache key + * + * @param {String} key + * @param {*} value + * @return void + */ + put(key, value) { + if (value === null || value === undefined) { + return; + } + + this._cache[key] = { + value, + expire_at: Date.now() + this._ttl + }; + + this._saveCache(); + } + + /** + * Retrieve value from cache by cache key or fetch new data + * + * @param {String} key + * @param {Function} callback + * @return {Promise<*>} + */ + get(key, callback) { + return new Promise((resolve, reject) => { + if (this.has(key)) { + resolve(this._cache[key].value); + return; + } + + this.delete(key); + + if (typeof callback !== 'function') { + reject(new Error('Callback must be a function')); + return; + } + + callback().then((response) => { + this.put(key, response); + resolve(response); + }).catch(reject); + }); + } + + /** + * Check if a valid value exists by cache key + * + * @param {String} key + * @return {Boolean} + */ + has(key) { + const data = this._cache[key]; + return data !== undefined && Date.now() < data.expire_at; + } + + /** + * Remove a value by cache key + * + * @param {String} key + * @return void + */ + delete(key) { + if (this._cache[key]) { + delete this._cache[key]; + this._saveCache(); + } + } + + /** + * Clear all cached data by tag + * + * @return void + */ + clear() { + this._cache = {}; + this._clearCache(); + } +} + +export default CacheFirstStrategy; \ No newline at end of file diff --git a/src/repositories/strategy/NetworkFirstStrategy.js b/src/repositories/strategy/NetworkFirstStrategy.js new file mode 100644 index 0000000..e6338cd --- /dev/null +++ b/src/repositories/strategy/NetworkFirstStrategy.js @@ -0,0 +1,105 @@ +import BaseStrategy from './BaseStrategy'; + +class NetworkFirstStrategy extends BaseStrategy { + /** + * @param {Object} config + * @param {Number} [config.ttl] + * @param {Object} [config.driver] + * @param {String} [config.cacheTag] + */ + constructor(config) { + super(config); + } + + /** + * Store a value by cache key + * + * @param {String} key + * @param {*} value + * @return void + */ + put(key, value) { + if (value === null || value === undefined) { + return; + } + + this._cache[key] = { + value, + expire_at: Date.now() + this._ttl + }; + + this._saveCache(); + } + + /** + * Tries network first, falls back to cache if failed. + * + * @param {String} key + * @param {Function} callback + * @return {Promise<*>} + */ + get(key, callback) { + return new Promise((resolve, reject) => { + if (typeof callback !== 'function') { + reject(new Error('Callback must be a function')); + return; + } + + const data = this._cache[key]; + + callback().then((response) => { + this.put(key, response); + resolve(response); + }).catch((reason) => { + if (data !== undefined) { + const { value, expire_at } = data; + + if (Date.now() < expire_at) { + resolve(value); + return; + } else { + this.delete(key); + } + } + + reject(reason); + }); + }); + } + + /** + * Check if a valid value exists by cache key + * + * @param {String} key + * @return {Boolean} + */ + has(key) { + const data = this._cache[key]; + return data !== undefined && Date.now() < data.expire_at; + } + + /** + * Remove a value by cache key + * + * @param {String} key + * @return void + */ + delete(key) { + if (this._cache[key]) { + delete this._cache[key]; + this._saveCache(); + } + } + + /** + * Clear all cached data by tag + * + * @return void + */ + clear() { + this._cache = {}; + this._clearCache(); + } +} + +export default NetworkFirstStrategy; \ No newline at end of file diff --git a/src/repositories/strategy/StaleWhileRevalidateStrategy.js b/src/repositories/strategy/StaleWhileRevalidateStrategy.js new file mode 100644 index 0000000..5e30c1c --- /dev/null +++ b/src/repositories/strategy/StaleWhileRevalidateStrategy.js @@ -0,0 +1,101 @@ +import BaseStrategy from './BaseStrategy'; + +class StaleWhileRevalidateStrategy extends BaseStrategy { + /** + * @param {Object} config + * @param {Number} [config.ttl] + * @param {Object} [config.driver] + * @param {String} [config.cacheTag] + */ + constructor(config) { + super(config); + } + + /** + * Store a value by cache key + * + * @param {String} key + * @param {*} value + * @return void + */ + put(key, value) { + if (value === null || value === undefined) { + return; + } + + this._cache[key] = { + value, + expire_at: Date.now() + this._ttl + }; + + this._saveCache(); + } + + /** + * Tries to fetch data from the cache first, and simultaneously revalidates the cache by fetching from the network. + * + * @param {String} key + * @param {Function} callback + * @return {Promise<*>} + */ + get(key, callback) { + return new Promise((resolve, reject) => { + if (this.has(key)) { + callback().then((response) => { + this.put(key, response); + }); + + resolve(this._cache[key].value); + return; + } + + this.delete(key); + + if (typeof callback !== 'function') { + reject(new Error('Callback must be a function')); + return; + } + + callback().then((response) => { + this.put(key, response); + resolve(response); + }).catch(reject); + }); + } + + /** + * Check if a valid value exists by cache key + * + * @param {String} key + * @return {Boolean} + */ + has(key) { + const data = this._cache[key]; + return data !== undefined && Date.now() < data.expire_at; + } + + /** + * Remove a value by cache key + * + * @param {String} key + * @return void + */ + delete(key) { + if (this._cache[key]) { + delete this._cache[key]; + this._saveCache(); + } + } + + /** + * Clear all cached data by tag + * + * @return void + */ + clear() { + this._cache = {}; + this._clearCache(); + } +} + +export default StaleWhileRevalidateStrategy; \ No newline at end of file diff --git a/src/services/todo.service.js b/src/repositories/todo.repository.js similarity index 62% rename from src/services/todo.service.js rename to src/repositories/todo.repository.js index 503dc62..762ec4b 100644 --- a/src/services/todo.service.js +++ b/src/repositories/todo.repository.js @@ -1,12 +1,12 @@ -import CrudService from './crud.service'; +import CrudRepository from './crud.repository'; -class TodoService extends CrudService { +class TodoRepository extends CrudRepository { /** - * Service url + * Repository url * * @returns {String} */ - static get URL() { + get URL() { return 'todos'; } @@ -15,7 +15,7 @@ class TodoService extends CrudService { * * @returns {Object} */ - static getDefault() { + getDefault() { return { userId: undefined, id: undefined, @@ -25,4 +25,4 @@ class TodoService extends CrudService { } } -export default TodoService; +export default new TodoRepository(); \ No newline at end of file diff --git a/src/services/user.service.js b/src/repositories/user.repository.js similarity index 81% rename from src/services/user.service.js rename to src/repositories/user.repository.js index a96ac53..ca6c1f9 100644 --- a/src/services/user.service.js +++ b/src/repositories/user.repository.js @@ -1,12 +1,12 @@ -import CrudService from './crud.service'; +import CrudRepository from './crud.repository'; -class UserService extends CrudService { +class UserRepository extends CrudRepository { /** - * Service url + * Repository url * * @returns {String} */ - static get URL() { + get URL() { return 'users'; } @@ -15,7 +15,7 @@ class UserService extends CrudService { * * @returns {Object} */ - static getDefault() { + getDefault() { return { id: undefined, name: undefined, @@ -42,4 +42,4 @@ class UserService extends CrudService { } } -export default UserService; +export default new UserRepository(); \ No newline at end of file diff --git a/src/services/authentication.service.js b/src/services/authentication.service.js index d4258fd..755c4ce 100644 --- a/src/services/authentication.service.js +++ b/src/services/authentication.service.js @@ -1,4 +1,5 @@ -import ApiService from './api.service'; +import ApiRepository from '@/repositories/api.repository'; + import TokenService from './token.service'; import PermissionService from './permission.service'; @@ -14,12 +15,12 @@ class AuthenticationService { * @returns {Promise} */ static login(data, config) { - return ApiService.post('users', data, config).then((response) => { + return ApiRepository.post('users', data, config).then((response) => { const token = btoa(JSON.stringify(response.data)); TokenService.set(token); PermissionService.set(['dashboard']); - ApiService.setHeader(HttpHeader.AUTHORIZATION, `Bearer ${TokenService.get()}`); + ApiRepository.setHeader(HttpHeader.AUTHORIZATION, `Bearer ${TokenService.get()}`); return response; }); diff --git a/src/services/language.service.js b/src/services/language.service.js index 5da1481..ebfa7c8 100644 --- a/src/services/language.service.js +++ b/src/services/language.service.js @@ -1,4 +1,4 @@ -import StorageService from './storage.service'; +import LocalStorageService from './local-storage.service'; import { createI18n as createVueI18N } from 'vue-i18n'; @@ -31,7 +31,7 @@ class LanguageService { * @returns void */ static set(value) { - StorageService.set(this.STORAGE_KEY, value); + LocalStorageService.set(this.STORAGE_KEY, value); location.reload(); } @@ -41,7 +41,7 @@ class LanguageService { * @returns {String} */ static get() { - return StorageService.get(this.STORAGE_KEY) ?? this.DEFAULT; + return LocalStorageService.get(this.STORAGE_KEY) ?? this.DEFAULT; } /** diff --git a/src/services/storage.service.js b/src/services/local-storage.service.js similarity index 82% rename from src/services/storage.service.js rename to src/services/local-storage.service.js index a784bf7..3db5e3b 100644 --- a/src/services/storage.service.js +++ b/src/services/local-storage.service.js @@ -1,4 +1,4 @@ -class StorageService { +class LocalStorageService { /** * Set an item * @@ -6,7 +6,7 @@ class StorageService { * @param {*} value * @returns void */ - static set(name, value) { + set(name, value) { const stringifyValue = JSON.stringify(value); localStorage.setItem(name, stringifyValue); } @@ -17,7 +17,7 @@ class StorageService { * @param {String} name * @returns {*} */ - static get(name) { + get(name) { const value = localStorage.getItem(name); return value ? JSON.parse(value) : undefined; } @@ -28,7 +28,7 @@ class StorageService { * @param {String} name * @returns void */ - static delete(name) { + delete(name) { localStorage.removeItem(name); } @@ -38,9 +38,9 @@ class StorageService { * @param {String} name * @returns {Boolean} */ - static has(name) { + has(name) { return Boolean(this.get(name)); } } -export default StorageService; +export default new LocalStorageService(); \ No newline at end of file diff --git a/src/services/permission.service.js b/src/services/permission.service.js index c62e10f..ae1f526 100644 --- a/src/services/permission.service.js +++ b/src/services/permission.service.js @@ -1,11 +1,11 @@ -import StorageService from './storage.service'; +import LocalStorageService from './local-storage.service'; class PermissionService { /** * @private * @type {Object} */ - static _permissions = StorageService.get(this.STORAGE_KEY) || {}; + static _permissions = LocalStorageService.get(this.STORAGE_KEY) || {}; /** * Storage key @@ -27,7 +27,7 @@ class PermissionService { this._permissions[permission] = true; } - StorageService.set(this.STORAGE_KEY, this._permissions); + LocalStorageService.set(this.STORAGE_KEY, this._permissions); } /** @@ -67,7 +67,7 @@ class PermissionService { */ static clear() { this._permissions = {}; - StorageService.delete(this.STORAGE_KEY); + LocalStorageService.delete(this.STORAGE_KEY); } } diff --git a/src/services/reserve.service.js b/src/services/reserve.service.js deleted file mode 100644 index 48c07f2..0000000 --- a/src/services/reserve.service.js +++ /dev/null @@ -1,25 +0,0 @@ -import ApiService from './api.service'; - -class ReserveService { - /** - * Service url - * - * @returns {String} - */ - static get URL() { - return 'reserves'; - } - - /** - * Get reserve by confirmation code - * - * @param {Number|String} confirmationCode - * @param {AxiosRequestConfig} [config] - * @returns {Promise} - */ - static getOneByConfirmationCode(confirmationCode, config) { - return ApiService.get(`${this.URL}/${confirmationCode}`, config); - } -} - -export default ReserveService; diff --git a/src/services/session-storage.service.js b/src/services/session-storage.service.js new file mode 100644 index 0000000..48fa5bc --- /dev/null +++ b/src/services/session-storage.service.js @@ -0,0 +1,46 @@ +class SessionStorageService { + /** + * Set an item + * + * @param {String} name + * @param {*} value + * @returns void + */ + set(name, value) { + const stringifyValue = JSON.stringify(value); + sessionStorage.setItem(name, stringifyValue); + } + + /** + * Get an item + * + * @param {String} name + * @returns {*} + */ + get(name) { + const value = sessionStorage.getItem(name); + return value ? JSON.parse(value) : undefined; + } + + /** + * Delete an item + * + * @param {String} name + * @returns void + */ + delete(name) { + sessionStorage.removeItem(name); + } + + /** + * Determine if an item exists + * + * @param {String} name + * @returns {Boolean} + */ + has(name) { + return Boolean(this.get(name)); + } +} + +export default new SessionStorageService(); \ No newline at end of file diff --git a/src/services/theme.service.js b/src/services/theme.service.js index 1c2ca34..fcffeb3 100644 --- a/src/services/theme.service.js +++ b/src/services/theme.service.js @@ -1,4 +1,4 @@ -import StorageService from './storage.service'; +import LocalStorageService from './local-storage.service'; class ThemeService { /** @@ -26,7 +26,7 @@ class ThemeService { * @returns void */ static set(value) { - StorageService.set(this.STORAGE_KEY, value); + LocalStorageService.set(this.STORAGE_KEY, value); this.updateDOM(); } @@ -46,7 +46,7 @@ class ThemeService { * @returns {String} */ static get() { - return StorageService.get(this.STORAGE_KEY) ?? this.DEFAULT; + return LocalStorageService.get(this.STORAGE_KEY) ?? this.DEFAULT; } } diff --git a/src/services/token.service.js b/src/services/token.service.js index 54a1300..3d7f450 100644 --- a/src/services/token.service.js +++ b/src/services/token.service.js @@ -1,4 +1,4 @@ -import StorageService from './storage.service'; +import LocalStorageService from './local-storage.service'; class TokenService { /** @@ -17,7 +17,7 @@ class TokenService { * @returns void */ static set(value) { - StorageService.set(this.STORAGE_KEY, value); + LocalStorageService.set(this.STORAGE_KEY, value); } /** @@ -26,7 +26,7 @@ class TokenService { * @returns {String} */ static get() { - return StorageService.get(this.STORAGE_KEY); + return LocalStorageService.get(this.STORAGE_KEY); } /** @@ -35,7 +35,7 @@ class TokenService { * @returns {Boolean} */ static isExist() { - return StorageService.has(this.STORAGE_KEY); + return LocalStorageService.has(this.STORAGE_KEY); } /** @@ -44,7 +44,7 @@ class TokenService { * @returns void */ static clear() { - StorageService.delete(this.STORAGE_KEY); + LocalStorageService.delete(this.STORAGE_KEY); } } diff --git a/src/views/UsersView.vue b/src/views/UsersView.vue index d73d2c6..ebf414a 100644 --- a/src/views/UsersView.vue +++ b/src/views/UsersView.vue @@ -39,5 +39,5 @@ users }; } - } + };