diff --git a/.gitignore b/.gitignore index 49eba67df9..e557c2d0c6 100755 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ node_modules.nosync/ # Dev tools .DS_Store .vscode +.cursor .idea *.swp *.bak diff --git a/apps/web/src/api-clients/_common/composables/use-api-query-key.ts b/apps/web/src/api-clients/_common/composables/use-api-query-key.ts index 1c92d0c5f8..99f7c1d2f4 100644 --- a/apps/web/src/api-clients/_common/composables/use-api-query-key.ts +++ b/apps/web/src/api-clients/_common/composables/use-api-query-key.ts @@ -1,31 +1,81 @@ -import { computed, reactive } from 'vue'; +import { reactive, computed } from 'vue'; + import type { - ResourceName, ServiceName, Verb, -} from '@/api-clients/_common/types/query-key-type'; + ServiceName, ResourceName, Verb, +} from '@/query/_types/query-key-type'; import { useAppContextStore } from '@/store/app-context/app-context-store'; import { useUserWorkspaceStore } from '@/store/app-context/workspace/user-workspace-store'; -/** - * Generates a computed query key for API requests, incorporating global parameters. - * - * @param service - The service name, representing the API service scope (e.g., 'dashboard'). - * @param resource - The resource name, specifying the target API resource (e.g., 'public-data-table'). - * @param verb - The API action verb, defining the type of request (e.g., 'get', 'list', 'update'). - * @param additionalGlobalParams - Optional additional global parameters (e.g., workspace ID, admin mode). - * @returns A computed reference to the query key array, structured as `[service, resource, verb, { globalParams }]`. - * - * ### Example Usage: - * ```ts - * const queryKey = useAPIQueryKey('dashboard', 'public-data-table', 'get'); - * ``` - * The generated query key ensures: - * - **Type safety**: Prevents invalid API calls by enforcing a valid `service/resource/verb` combination. - * - **Auto-completion**: Provides intelligent suggestions based on predefined API structure. - * - **Cache management**: Enables precise cache invalidation and data synchronization. - */ +// type QueryKeyArrayWithDep = QueryKeyArray & { +// addDep: (deps: Record) => QueryKeyArray; +// }; +// type ExtractParams = T extends (params: infer P) => any ? P : never; + +// type VerbFunction = { +// (params?: ExtractParams): QueryKeyArrayWithDep; +// }; + +// type MapVerbToReturnType = T extends (params: any) => any +// ? VerbFunction +// : never; + +// type UseAPIQueryResult = { +// [S in APIQueryKeyMapService]: { +// [R in APIQueryKeyMapResource]: { +// [V in APIQueryKeyMapVerb]: MapVerbToReturnType<(typeof API_QUERY_KEY_MAP)[S][R][V]>; +// }; +// }; +// }; + +// type APIQueryKeyValue = (params?: Record) => QueryKeyArray; + + +// export const _useAPIQueryKey = (): ComputedRef => { +// const queryKeyAppContext = useQueryKeyAppContext('service'); +// const globalContext = computed(() => queryKeyAppContext.value); + + +// const apiStructure = Object.entries(API_QUERY_KEY_MAP).reduce>((result, [serviceName, resources]) => { +// result[serviceName as APIQueryKeyMapService] = Object.entries(resources).reduce((resourceResult, [resourceName, verbs]) => { +// resourceResult[resourceName] = Object.entries(verbs).reduce((verbResult, [verb, queryKeyValue]) => { +// const staticKey = createImmutableObject([serviceName, resourceName, verb]); + +// const verbFunction = (params?: ExtractParams) => { +// const baseKey = computed(() => (queryKeyValue as T)(params)); +// const queryKey = [ +// ...globalContext.value, +// ...staticKey, +// ...createImmutableObject(baseKey.value), +// ] as QueryKeyArray; + +// (queryKey as QueryKeyArrayWithDep).addDep = (deps: Record): QueryKeyArray => [ +// ...globalContext.value, +// ...staticKey, +// ...createImmutableObject(baseKey.value), +// createImmutableObject(deps), +// ]; + +// return queryKey as QueryKeyArrayWithDep; +// }; + +// verbResult[verb] = verbFunction; +// return verbResult; +// }, {}); +// return resourceResult; +// }, {}); +// return result; +// }, {} as Record); + + +// return computed(() => apiStructure as UseAPIQueryResult); +// }; + + + +// TODO: Deprecate this interface GlobalQueryParams { workspaceId?: string; isAdminMode?: boolean; @@ -52,3 +102,7 @@ export const useAPIQueryKey = , return computed(() => [service, resource, verb, { ...globalQueryParams }]); }; + + + + diff --git a/apps/web/src/api-clients/_common/composables/use-scoped-query.ts b/apps/web/src/api-clients/_common/composables/use-scoped-query.ts index 0077392792..1b114ee946 100644 --- a/apps/web/src/api-clients/_common/composables/use-scoped-query.ts +++ b/apps/web/src/api-clients/_common/composables/use-scoped-query.ts @@ -1,5 +1,3 @@ -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-nocheck /** * useScopedQuery - A custom wrapper around `useQuery` to enforce scope-based API fetching. * @@ -10,8 +8,8 @@ * * ## Functionality * - Extends `useQuery` with **grant scope validation**. - * - Runs queries only when **the user’s scope is valid** and the app is **ready**. - * - Uses Vue’s **reactivity** to dynamically compute the `enabled` state. + * - Runs queries only when **the user's scope is valid** and the app is **ready**. + * - Uses Vue's **reactivity** to dynamically compute the `enabled` state. * - Supports both **static and reactive `enabled` values**. * * ## Parameters: @@ -43,7 +41,7 @@ import type { MaybeRef } from '@vueuse/core'; import { toValue } from '@vueuse/core'; -import { computed, reactive } from 'vue'; +import { computed } from 'vue'; import type { UseQueryOptions, @@ -57,21 +55,19 @@ import { useUserStore } from '@/store/user/user-store'; export const useScopedQuery = ( options: UseQueryOptions, - requiredScopes: GrantScope[] = [], + requiredScopes: GrantScope[], ) => { const appContextStore = useAppContextStore(); const userStore = useUserStore(); - const _state = reactive({ - currentGrantScope: computed(() => userStore.state.currentGrantInfo?.scope || 'USER'), - isLoading: computed(() => appContextStore.getters.globalGrantLoading), - isValidScope: computed(() => requiredScopes.includes(_state.currentGrantScope)), - }); + const currentGrantScope = computed(() => userStore.state.currentGrantInfo?.scope || 'USER'); + const isLoading = computed(() => appContextStore.getters.globalGrantLoading); + const isValidScope = computed(() => requiredScopes.includes(currentGrantScope.value)); const queryEnabled = computed(() => { - const _inheritedEnabled = options?.enabled as MaybeRef | undefined; + const _inheritedEnabled = ('enabled' in options ? options.enabled : undefined) as MaybeRef | undefined; if (_inheritedEnabled !== undefined && !toValue(_inheritedEnabled)) return false; - return _state.isValidScope && !_state.isLoading; + return isValidScope.value && !isLoading.value; }); return useQuery({ diff --git a/apps/web/src/api-clients/_common/constants/api-query-key-map-constant.ts b/apps/web/src/api-clients/_common/constants/api-query-key-map-constant.ts new file mode 100644 index 0000000000..0d015b07f1 --- /dev/null +++ b/apps/web/src/api-clients/_common/constants/api-query-key-map-constant.ts @@ -0,0 +1,33 @@ +import { privateDashboardKeys } from '@/api-clients/dashboard/private-dashboard/keys'; +import { privateDataTableKeys } from '@/api-clients/dashboard/private-data-table/keys'; +import { privateFolderKeys } from '@/api-clients/dashboard/private-folder/keys'; +import { privateWidgetKeys } from '@/api-clients/dashboard/private-widget/keys'; +import { publicDashboardKeys } from '@/api-clients/dashboard/public-dashboard/keys'; +import { publicDataTableKeys } from '@/api-clients/dashboard/public-data-table/keys'; +import { publicFolderKeys } from '@/api-clients/dashboard/public-folder/keys'; +import { publicWidgetKeys } from '@/api-clients/dashboard/public-widget/keys'; +import { commentKeys } from '@/api-clients/opsflow/comment/keys'; +import { eventKeys } from '@/api-clients/opsflow/event/keys'; +import { taskCategoryKeys } from '@/api-clients/opsflow/task-category/keys'; +import { taskTypeKeys } from '@/api-clients/opsflow/task-type/keys'; +import { taskKeys } from '@/api-clients/opsflow/task/keys'; + +export const API_QUERY_KEY_MAP = { + dashboard: { + publicFolder: publicFolderKeys, + publicDashboard: publicDashboardKeys, + publicDataTable: publicDataTableKeys, + publicWidget: publicWidgetKeys, + privateFolder: privateFolderKeys, + privateDashboard: privateDashboardKeys, + privateDataTable: privateDataTableKeys, + privateWidget: privateWidgetKeys, + }, + opsflow: { + comment: commentKeys, + task: taskKeys, + taskCategory: taskCategoryKeys, + taskType: taskTypeKeys, + event: eventKeys, + }, +} as const; diff --git a/apps/web/src/api-clients/_common/helpers/service-query-invalidator-helper.ts b/apps/web/src/api-clients/_common/helpers/service-query-invalidator-helper.ts new file mode 100644 index 0000000000..d249d989af --- /dev/null +++ b/apps/web/src/api-clients/_common/helpers/service-query-invalidator-helper.ts @@ -0,0 +1,7 @@ +import { apiQueryClient } from '@/query/clients'; + +export const invalidateServiceQuery = async (mode: 'admin' | 'workspace') => { + await apiQueryClient.invalidateQueries({ + queryKey: ['service', mode], + }); +}; diff --git a/apps/web/src/api-clients/dashboard/private-dashboard/keys.ts b/apps/web/src/api-clients/dashboard/private-dashboard/keys.ts new file mode 100644 index 0000000000..25d1828adc --- /dev/null +++ b/apps/web/src/api-clients/dashboard/private-dashboard/keys.ts @@ -0,0 +1,9 @@ +import type { PrivateDashboardGetParameters } from '@/api-clients/dashboard/private-dashboard/schema/api-verbs/get'; +import type { PrivateDashboardListParameters } from '@/api-clients/dashboard/private-dashboard/schema/api-verbs/list'; + + + +export const privateDashboardKeys = { + list: (params: PrivateDashboardListParameters) => ['private-dashboard', 'list', params] as const, + get: (idParam: PrivateDashboardGetParameters['dashboard_id']) => ['private-dashboard', 'get', idParam] as const, +}; diff --git a/apps/web/src/api-clients/dashboard/private-data-table/keys.ts b/apps/web/src/api-clients/dashboard/private-data-table/keys.ts new file mode 100644 index 0000000000..2c78533c96 --- /dev/null +++ b/apps/web/src/api-clients/dashboard/private-data-table/keys.ts @@ -0,0 +1,10 @@ + +import type { DataTableGetParameters } from '@/api-clients/dashboard/private-data-table/schema/api-verbs/get'; +import type { DataTableListParameters } from '@/api-clients/dashboard/private-data-table/schema/api-verbs/list'; +import type { DataTableLoadParameters } from '@/api-clients/dashboard/private-data-table/schema/api-verbs/load'; + +export const privateDataTableKeys = { + list: (params: DataTableListParameters) => ['private-data-table', 'list', params] as const, + get: (idParam: DataTableGetParameters['data_table_id']) => ['private-data-table', 'get', idParam] as const, + load: (idParam: DataTableLoadParameters['data_table_id']) => ['private-data-table', 'load', idParam] as const, +}; diff --git a/apps/web/src/api-clients/dashboard/private-folder/keys.ts b/apps/web/src/api-clients/dashboard/private-folder/keys.ts new file mode 100644 index 0000000000..83d9236d8d --- /dev/null +++ b/apps/web/src/api-clients/dashboard/private-folder/keys.ts @@ -0,0 +1,7 @@ +import type { PrivateFolderGetParameters } from '@/api-clients/dashboard/private-folder/schema/api-verbs/get'; +import type { PrivateFolderListParameters } from '@/api-clients/dashboard/private-folder/schema/api-verbs/list'; + +export const privateFolderKeys = { + list: (params: PrivateFolderListParameters) => ['private-folder', 'list', params] as const, + get: (idParam: PrivateFolderGetParameters['folder_id']) => ['private-folder', 'get', idParam] as const, +}; diff --git a/apps/web/src/api-clients/dashboard/private-widget/keys.ts b/apps/web/src/api-clients/dashboard/private-widget/keys.ts new file mode 100644 index 0000000000..14479b4b11 --- /dev/null +++ b/apps/web/src/api-clients/dashboard/private-widget/keys.ts @@ -0,0 +1,11 @@ +import type { PrivateWidgetGetParameters } from '@/api-clients/dashboard/private-widget/schema/api-verbs/get'; +import type { PrivateWidgetListParameters } from '@/api-clients/dashboard/private-widget/schema/api-verbs/list'; +import type { PrivateWidgetLoadParameters } from '@/api-clients/dashboard/private-widget/schema/api-verbs/load'; +import type { PrivateWidgetLoadSumParameters } from '@/api-clients/dashboard/private-widget/schema/api-verbs/load-sum'; + +export const privateWidgetKeys = { + list: (params: PrivateWidgetListParameters) => ['private-widget', 'list', params] as const, + get: (idParam: PrivateWidgetGetParameters['widget_id']) => ['private-widget', 'get', idParam] as const, + load: (idParam: PrivateWidgetLoadParameters['widget_id']) => ['private-widget', 'load', idParam] as const, + loadSum: (idParam: PrivateWidgetLoadSumParameters['widget_id']) => ['private-widget', 'load-sum', idParam] as const, +}; diff --git a/apps/web/src/api-clients/dashboard/public-dashboard/keys.ts b/apps/web/src/api-clients/dashboard/public-dashboard/keys.ts new file mode 100644 index 0000000000..36f5611bf3 --- /dev/null +++ b/apps/web/src/api-clients/dashboard/public-dashboard/keys.ts @@ -0,0 +1,7 @@ +import type { PublicDashboardGetParameters } from '@/api-clients/dashboard/public-dashboard/schema/api-verbs/get'; +import type { PublicDashboardListParameters } from '@/api-clients/dashboard/public-dashboard/schema/api-verbs/list'; + +export const publicDashboardKeys = { + list: (params: PublicDashboardListParameters) => ['public-dashboard', 'list', params] as const, + get: (idParam: PublicDashboardGetParameters['dashboard_id']) => ['public-dashboard', 'get', idParam] as const, +}; diff --git a/apps/web/src/api-clients/dashboard/public-data-table/keys.ts b/apps/web/src/api-clients/dashboard/public-data-table/keys.ts new file mode 100644 index 0000000000..ce17c1f617 --- /dev/null +++ b/apps/web/src/api-clients/dashboard/public-data-table/keys.ts @@ -0,0 +1,9 @@ +import type { DataTableGetParameters } from '@/api-clients/dashboard/public-data-table/schema/api-verbs/get'; +import type { DataTableListParameters } from '@/api-clients/dashboard/public-data-table/schema/api-verbs/list'; +import type { DataTableLoadParameters } from '@/api-clients/dashboard/public-data-table/schema/api-verbs/load'; + +export const publicDataTableKeys = { + list: (params: DataTableListParameters) => ['public-data-table', 'list', params] as const, + get: (idParam: DataTableGetParameters['data_table_id']) => ['public-data-table', 'get', idParam] as const, + load: (idParam: DataTableLoadParameters['data_table_id']) => ['public-data-table', 'load', idParam] as const, +}; diff --git a/apps/web/src/api-clients/dashboard/public-folder/keys.ts b/apps/web/src/api-clients/dashboard/public-folder/keys.ts new file mode 100644 index 0000000000..49ca9a3931 --- /dev/null +++ b/apps/web/src/api-clients/dashboard/public-folder/keys.ts @@ -0,0 +1,7 @@ +import type { PublicFolderGetParameters } from '@/api-clients/dashboard/public-folder/schema/api-verbs/get'; +import type { PublicFolderListParameters } from '@/api-clients/dashboard/public-folder/schema/api-verbs/list'; + +export const publicFolderKeys = { + list: (params: PublicFolderListParameters) => ['public-folder', 'list', params] as const, + get: (idParam: PublicFolderGetParameters['folder_id']) => ['public-folder', 'get', idParam] as const, +}; diff --git a/apps/web/src/api-clients/dashboard/public-widget/keys.ts b/apps/web/src/api-clients/dashboard/public-widget/keys.ts new file mode 100644 index 0000000000..01981f58c5 --- /dev/null +++ b/apps/web/src/api-clients/dashboard/public-widget/keys.ts @@ -0,0 +1,11 @@ +import type { PublicWidgetGetParameters } from '@/api-clients/dashboard/public-widget/schema/api-verbs/get'; +import type { PublicWidgetListParameters } from '@/api-clients/dashboard/public-widget/schema/api-verbs/list'; +import type { PublicWidgetLoadParameters } from '@/api-clients/dashboard/public-widget/schema/api-verbs/load'; +import type { PublicWidgetLoadSumParameters } from '@/api-clients/dashboard/public-widget/schema/api-verbs/load-sum'; + +export const publicWidgetKeys = { + list: (params: PublicWidgetListParameters) => ['public-widget', 'list', params] as const, + get: (idParam: PublicWidgetGetParameters['widget_id']) => ['public-widget', 'get', idParam] as const, + load: (idParam: PublicWidgetLoadParameters['widget_id']) => ['public-widget', 'load', idParam] as const, + loadSum: (idParam: PublicWidgetLoadSumParameters['widget_id']) => ['public-widget', 'load-sum', idParam] as const, +}; diff --git a/apps/web/src/api-clients/opsflow/comment/keys.ts b/apps/web/src/api-clients/opsflow/comment/keys.ts new file mode 100644 index 0000000000..43262e155b --- /dev/null +++ b/apps/web/src/api-clients/opsflow/comment/keys.ts @@ -0,0 +1,7 @@ +import type { CommentGetParameters } from '@/api-clients/opsflow/comment/schema/api-verbs/get'; +import type { CommentListParameters } from '@/api-clients/opsflow/comment/schema/api-verbs/list'; + +export const commentKeys = { + list: (params: CommentListParameters) => ['comment', 'list', params] as const, + get: (idParam: CommentGetParameters['comment_id']) => ['comment', 'get', idParam] as const, +}; diff --git a/apps/web/src/api-clients/opsflow/event/keys.ts b/apps/web/src/api-clients/opsflow/event/keys.ts new file mode 100644 index 0000000000..d8fb908897 --- /dev/null +++ b/apps/web/src/api-clients/opsflow/event/keys.ts @@ -0,0 +1,5 @@ +import type { EventListParameters } from '@/api-clients/opsflow/event/schema/api-verbs/list'; + +export const eventKeys = { + list: (params: EventListParameters) => ['event', 'list', params] as const, +}; diff --git a/apps/web/src/api-clients/opsflow/task-category/keys.ts b/apps/web/src/api-clients/opsflow/task-category/keys.ts new file mode 100644 index 0000000000..24a1db8427 --- /dev/null +++ b/apps/web/src/api-clients/opsflow/task-category/keys.ts @@ -0,0 +1,7 @@ +import type { TaskCategoryGetParameters } from '@/api-clients/opsflow/task-category/schema/api-verbs/get'; +import type { TaskCategoryListParameters } from '@/api-clients/opsflow/task-category/schema/api-verbs/list'; + +export const taskCategoryKeys = { + list: (params: TaskCategoryListParameters) => ['task-category', 'list', params] as const, + get: (idParam: TaskCategoryGetParameters['category_id']) => ['task-category', 'get', idParam] as const, +}; diff --git a/apps/web/src/api-clients/opsflow/task-type/keys.ts b/apps/web/src/api-clients/opsflow/task-type/keys.ts new file mode 100644 index 0000000000..3e65518d55 --- /dev/null +++ b/apps/web/src/api-clients/opsflow/task-type/keys.ts @@ -0,0 +1,7 @@ +import type { TaskTypeGetParameters } from '@/api-clients/opsflow/task-type/schema/api-verbs/get'; +import type { TaskTypeListParameters } from '@/api-clients/opsflow/task-type/schema/api-verbs/list'; + +export const taskTypeKeys = { + list: (params: TaskTypeListParameters) => ['task-type', 'list', params] as const, + get: (idParam: TaskTypeGetParameters['task_type_id']) => ['task-type', 'get', idParam] as const, +}; diff --git a/apps/web/src/api-clients/opsflow/task/keys.ts b/apps/web/src/api-clients/opsflow/task/keys.ts new file mode 100644 index 0000000000..7969ccadb2 --- /dev/null +++ b/apps/web/src/api-clients/opsflow/task/keys.ts @@ -0,0 +1,7 @@ +import type { TaskGetParameters } from '@/api-clients/opsflow/task/schema/api-verbs/get'; +import type { TaskListParameters } from '@/api-clients/opsflow/task/schema/api-verbs/list'; + +export const taskKeys = { + list: (params: TaskListParameters) => ['task', 'list', params] as const, + get: (idParam: TaskGetParameters['task_id']) => ['task', 'get', idParam] as const, +}; diff --git a/apps/web/src/main.ts b/apps/web/src/main.ts index 3d8cc3687e..5a56871cdf 100644 --- a/apps/web/src/main.ts +++ b/apps/web/src/main.ts @@ -10,6 +10,7 @@ import VTooltip from 'v-tooltip'; import SpaceDesignSystem from '@cloudforet/mirinae'; import directive from '@/directives'; +import { apiQueryClient } from '@/query/clients'; import { SpaceRouter } from '@/router'; import { i18n } from '@/translations'; @@ -19,6 +20,7 @@ import { siteInit } from '@/lib/site-initializer'; import App from './App.vue'; + import '@/styles/style.pcss'; // eslint-disable-next-line import '@cloudforet/mirinae/css/light-style.css'; @@ -30,7 +32,8 @@ Vue.use(Fragment.Plugin); Vue.use(VTooltip, { defaultClass: 'p-tooltip', defaultBoundariesElement: document.body }); Vue.use(PortalVue); Vue.use(PiniaVuePlugin); -Vue.use(VueQueryPlugin); +Vue.use(VueQueryPlugin, { queryClient: apiQueryClient }); + directive(Vue); diff --git a/apps/web/src/query/_composables/use-api-query-key.ts b/apps/web/src/query/_composables/use-api-query-key.ts new file mode 100644 index 0000000000..e7662a1679 --- /dev/null +++ b/apps/web/src/query/_composables/use-api-query-key.ts @@ -0,0 +1,47 @@ +import { toValue } from '@vueuse/core'; +import type { Ref } from 'vue'; +import { computed } from 'vue'; + +import { useQueryKeyAppContext } from '@/query/_composables/use-query-key-app-context'; +import { createImmutableObjectKeyItem } from '@/query/_helpers/immutable-key-helper'; +import type { ResourceName, ServiceName, Verb } from '@/query/_types/query-key-type'; + + +type _MaybeRefOrGetter = T | Ref | (() => T); + +type UseAPIQueryKeyOptions = { + id?: _MaybeRefOrGetter; + params: _MaybeRefOrGetter; + deps?: _MaybeRefOrGetter; +}; + +export const useAPIQueryKey = , V extends Verb>( + service: S, + resource: R, + verb: V, + options: UseAPIQueryKeyOptions, +) => { + const { id, params, deps } = options; + + const queryKeyAppContext = useQueryKeyAppContext(); + const globalContext = computed(() => queryKeyAppContext.value); + + const queryKey = computed(() => { + const resolvedParams = toValue(params); + const resolvedDeps = toValue(deps); + const resolvedId = id ? toValue(id) : undefined; + + return [ + ...globalContext.value, + service, resource, verb, + ...(resolvedId ? [resolvedId] : []), + createImmutableObjectKeyItem(resolvedParams), + ...(resolvedDeps ? [createImmutableObjectKeyItem(resolvedDeps)] : []), + ]; + }); + + return { + key: queryKey, + params: computed(() => toValue(params)), + }; +}; diff --git a/apps/web/src/query/_composables/use-query-key-app-context.ts b/apps/web/src/query/_composables/use-query-key-app-context.ts new file mode 100644 index 0000000000..0077545a53 --- /dev/null +++ b/apps/web/src/query/_composables/use-query-key-app-context.ts @@ -0,0 +1,44 @@ +import { computed, reactive } from 'vue'; + +import { useAppContextStore } from '@/store/app-context/app-context-store'; +import { useUserWorkspaceStore } from '@/store/app-context/workspace/user-workspace-store'; + + + + +interface AdminModeState { + isAdminMode: true; + workspaceId?: undefined; +} + +interface WorkspaceModeState { + isAdminMode: false; + workspaceId: string; +} + +type QueryKeyState = AdminModeState | WorkspaceModeState; + + +export const useQueryKeyAppContext = () => { + const appContextStore = useAppContextStore(); + const userWorkspaceStore = useUserWorkspaceStore(); + + const _state = reactive({ + isAdminMode: computed(() => appContextStore.getters.isAdminMode), + workspaceId: computed(() => userWorkspaceStore.getters.currentWorkspaceId), + }); + + return computed(() => { + const state: QueryKeyState = _state.isAdminMode + ? { isAdminMode: true } + : { + isAdminMode: false, + // workspaceId: _state.workspaceId! + workspaceId: _state.workspaceId ?? '', + }; + + return state.isAdminMode + ? ['admin'] + : ['workspace', state.workspaceId]; + }); +}; diff --git a/apps/web/src/query/_helpers/immutable-key-helper.ts b/apps/web/src/query/_helpers/immutable-key-helper.ts new file mode 100644 index 0000000000..9de930582d --- /dev/null +++ b/apps/web/src/query/_helpers/immutable-key-helper.ts @@ -0,0 +1,16 @@ +export const createImmutableObjectKeyItem = >(obj: T): T => { + if (obj === null || typeof obj !== 'object') { + return obj; + } + + if (Array.isArray(obj)) { + return obj.map((item) => createImmutableObjectKeyItem(item)) as unknown as T; + } + + const immutableObj = Object.entries(obj).reduce((acc, [key, value]) => ({ + ...acc, + [key]: createImmutableObjectKeyItem(value), + }), {}); + + return Object.freeze(immutableObj) as T; +}; diff --git a/apps/web/src/api-clients/_common/types/query-key-type.ts b/apps/web/src/query/_types/query-key-type.ts similarity index 81% rename from apps/web/src/api-clients/_common/types/query-key-type.ts rename to apps/web/src/query/_types/query-key-type.ts index b8430ca461..1a647835df 100644 --- a/apps/web/src/api-clients/_common/types/query-key-type.ts +++ b/apps/web/src/query/_types/query-key-type.ts @@ -1,6 +1,12 @@ import type { API_DOC } from '@/api-clients/_common/constants/api-doc'; +export type QueryKeyArray = unknown[]; + +export type QueryScope = 'service' | 'reference'; + + + /** * Extracts all possible keys for `{service-name}`, `{resource-name}`, and `{verb}` */ diff --git a/apps/web/src/query/clients.ts b/apps/web/src/query/clients.ts new file mode 100644 index 0000000000..b12aa51602 --- /dev/null +++ b/apps/web/src/query/clients.ts @@ -0,0 +1,5 @@ +import { QueryClient } from '@tanstack/vue-query'; + +export const apiQueryClient = new QueryClient(); + +export const referenceQueryClient = new QueryClient(); diff --git a/apps/web/src/query/reference/_composables/use-reference-advance-query.ts b/apps/web/src/query/reference/_composables/use-reference-advance-query.ts new file mode 100644 index 0000000000..4e0738a891 --- /dev/null +++ b/apps/web/src/query/reference/_composables/use-reference-advance-query.ts @@ -0,0 +1,164 @@ +import { useQuery, useQueryClient } from '@tanstack/vue-query'; + +import type { ListResponse } from '@/api-clients/_common/schema/api-verbs/list'; + +import { REFERENCE_TYPE_INFO_MAP } from '../_constants/reference-type-map-constant'; +import type { ReferenceItem, ReferenceMap } from '../_types/reference-type'; +import { useReferenceQueryKey } from './use-reference-query-key'; +import { computed } from 'vue'; + +interface ReferenceQueryOptions, T extends ReferenceItem> { + resourceType: keyof typeof REFERENCE_TYPE_INFO_MAP; + queryFn: (params: any) => Promise>; + getItems: (params: any) => Promise>; + selectFn: (data: R) => T; +} + +/* +interface PublicDashboardReferenceQuery { + list: PublicDashboard[]; + map: Record>>; + // isLoading: boolean + // refetch: () => void; + // invalidate: () => void; +} +*/ + +export const useAdvancedReferenceQuery = , T extends ReferenceItem>(options: ReferenceQueryOptions) => { + const { resourceType, queryFn, getItems, selectFn } = options; + const queryClient = useQueryClient(); + const _queryKey = useReferenceQueryKey(); + + const mapQuery = useQuery({ + queryKey: _queryKey.value[resourceType], + queryFn: async (params: any) => { + const response = await queryFn(params); + + + return response; + }, + select: (data) => { + if (!data.results?.length) return {}; + + return { + raw: data.results, + getItem: (id: string) => { + const item = data.results?.find(item => item.id === id); + return item ? selectFn(item) : undefined; + }, + getItems: (ids: string[]) => { + return ids.map(id => { + const item = data.results?.find(item => item.id === id); + return item ? selectFn(item) : undefined; + }); + } + }; + }, + }); + + + const pendingRequests = new Map>(); + const batchQueue = new Set(); + let batchTimer: NodeJS.Timeout | null = null; + + const getReferenceItem = async (id: string) => { + const cached = mainQuery.data?.[id]; + if (cached) return cached; + + if (pendingRequests.has(id)) { + return pendingRequests.get(id); + } + + const promise = new Promise((resolve, reject) => { + batchQueue.add(id); + + if (!batchTimer) { + batchTimer = setTimeout(async () => { + const ids = Array.from(batchQueue); + batchQueue.clear(); + + + try { + return useQuery({ + queryKey: [_queryKey.value[options.resourceType], 'item', id], + queryFn: async () => getItems({ + query: { + filter: [{ + k: REFERENCE_TYPE_INFO_MAP[resourceType].key, + v: ids, + }], + }, + }), + select: (data) => { + data.results?.forEach((item) => { + queryClient.setQueryData([_queryKey.value[resourceType]], (old: ListResponse) => { + if (!old.results?.length) return { results: [item] }; + return { + ...old, + results: [...old.results, item], + }; + }); + }); + return referenceMap; + }, + staleTime: 0, + }); + + // const response = await getItems({ + // query: { + // filter: [{ + // k: REFERENCE_TYPE_INFO_MAP[resourceType].key, + // v: ids, + // }], + // }, + // }); + // response.results?.forEach((item) => { + // queryClient.setQueryData([_queryKey.value[resourceType]], (old: ListResponse) => { + // if (!old.results?.length) return { results: [item] }; + // return { + // ...old, + // results: [...old.results, item], + // }; + // }); + // }); + } catch (error) { + reject(error); + } finally { + pendingRequests.delete(id); + } + }, 50); + } + }); + + pendingRequests.set(id, promise); + }; + + + + const data = computed(() => { + get(id) { + return mainQuery.data.value.?[id]; + } + + + + }); + + + // 4. 반환값 + return { + // 데이터 + data: mainQuery.data, + + // 상태 + isLoading: mainQuery.isLoading, + isError: mainQuery.isError, + error: mainQuery.error, + + // 메서드 + getReferenceItem, + + // 원본 쿼리 객체 (필요시 접근) + query: mainQuery, + }; +}; diff --git a/apps/web/src/query/reference/_composables/use-reference-query-key.ts b/apps/web/src/query/reference/_composables/use-reference-query-key.ts new file mode 100644 index 0000000000..1f07141386 --- /dev/null +++ b/apps/web/src/query/reference/_composables/use-reference-query-key.ts @@ -0,0 +1,17 @@ +import type { ComputedRef } from 'vue'; +import { computed } from 'vue'; + +import { useQueryKeyAppContext } from '@/query/_composables/use-query-key-app-context'; +import type { QueryKeyArray } from '@/query/_types/query-key-type'; +import { REFERENCE_TYPE_INFO_MAP } from '@/query/reference/_constants/reference-type-map-constant'; + +type UseReferenceQueryKeyResult = Record; + +export const useReferenceQueryKey = (): ComputedRef => { + const queryKeyAppContext = useQueryKeyAppContext(); + + return computed(() => Object.entries(REFERENCE_TYPE_INFO_MAP).reduce((acc, [key, value]) => ({ + ...acc, + [key]: [queryKeyAppContext.value, value], + }), {} as UseReferenceQueryKeyResult)); +}; diff --git a/apps/web/src/query/reference/_composables/use-reference-query-sync.ts b/apps/web/src/query/reference/_composables/use-reference-query-sync.ts new file mode 100644 index 0000000000..16e5840c79 --- /dev/null +++ b/apps/web/src/query/reference/_composables/use-reference-query-sync.ts @@ -0,0 +1,71 @@ +import type { QueryClient } from '@tanstack/vue-query'; +import { useQueryClient } from '@tanstack/vue-query'; + +import type { ListResponse } from '@/api-clients/_common/schema/api-verbs/list'; +import type { QueryKeyArray } from '@/query/_types/query-key-type'; +import { useReferenceQueryKey } from '@/query/reference/_composables/use-reference-query-key'; +import { REFERENCE_TYPE_INFO_MAP } from '@/query/reference/_constants/reference-type-map-constant'; + + +type MutationCallback = () => Promise; + +export const useReferenceQuerySync = (resourceType: keyof typeof REFERENCE_TYPE_INFO_MAP) => { + const queryClient = useQueryClient(); + + const withReferenceUpdate = async ( + mutationCallback: MutationCallback, + ): Promise => { + const response = await mutationCallback(); + const referenceQueryKey = useReferenceQueryKey(); + const queryKey = referenceQueryKey.value[resourceType]; + await updateReferenceCache(queryClient, queryKey, response, resourceType); + return response; + }; + + const withReferenceRefresh = async ( + mutationCallback: MutationCallback, + ): Promise => { + await mutationCallback(); + const referenceQueryKey = useReferenceQueryKey(); + const queryKey = referenceQueryKey.value[resourceType]; + await queryClient.invalidateQueries({ queryKey }); + }; + + return { withReferenceUpdate, withReferenceRefresh }; +}; + + +const updateReferenceCache = async ( + queryClient: QueryClient, + queryKey: QueryKeyArray, + newData: T, + resourceType: keyof typeof REFERENCE_TYPE_INFO_MAP, +) => { + const resourceKey = REFERENCE_TYPE_INFO_MAP[resourceType].key; + + if (!resourceKey || !newData[resourceKey]) { + throw new Error(`Invalid resource key or data for type: ${resourceType}`); + } + + queryClient.setQueryData>(queryKey, (oldData: ListResponse | undefined) => { + const currentResults = oldData?.results ?? []; + + if (newData == null) { + return oldData; + } + + const existingItemIndex = currentResults.findIndex( + (item) => item?.[resourceKey] === newData[resourceKey], + ); + + if (existingItemIndex > -1) { + const updatedResults = [...currentResults]; + updatedResults[existingItemIndex] = newData; + return { results: updatedResults }; + } + + return { + results: [...currentResults, newData], + }; + }); +}; diff --git a/apps/web/src/query/reference/_composables/use-reference-query.ts b/apps/web/src/query/reference/_composables/use-reference-query.ts new file mode 100644 index 0000000000..8f5484bb51 --- /dev/null +++ b/apps/web/src/query/reference/_composables/use-reference-query.ts @@ -0,0 +1,39 @@ +import { toValue, type MaybeRef } from '@vueuse/core'; +import { computed } from 'vue'; + +import { useQuery, type UseQueryOptions } from '@tanstack/vue-query'; + +import type { APIError } from '@cloudforet/core-lib/space-connector/error'; + +import type { ListResponse } from '@/api-clients/_common/schema/api-verbs/list'; + +import { useAppContextStore } from '@/store/app-context/app-context-store'; + + +const REFERENCE_LOAD_TTL = 1000 * 60 * 60 * 3; // 3 hours +const INITIAL_LIST_RESPONSE_DATA: ListResponse = { results: [] }; + +export const useReferenceQuery = , TError = unknown, TData = TQueryFnData>( + options: UseQueryOptions, +) => { + const appContextStore = useAppContextStore(); + + const queryEnabled = computed(() => { + const _inheritedEnabled = ('enabled' in options ? options.enabled : undefined) as MaybeRef | undefined; + if (_inheritedEnabled !== undefined && !toValue(_inheritedEnabled)) return false; + return !appContextStore.getters.globalGrantLoading; + }); + + return useQuery({ + ...options, + enabled: queryEnabled, + staleTime: REFERENCE_LOAD_TTL, + retry: (failureCount, err) => { + if ((err as APIError).status === 404) { + return false; + } + return failureCount < 3; + }, + initialData: (() => INITIAL_LIST_RESPONSE_DATA as TQueryFnData), + }); +}; diff --git a/apps/web/src/query/reference/_constants/reference-type-map-constant.ts b/apps/web/src/query/reference/_constants/reference-type-map-constant.ts new file mode 100644 index 0000000000..a70a252c89 --- /dev/null +++ b/apps/web/src/query/reference/_constants/reference-type-map-constant.ts @@ -0,0 +1,107 @@ +export const REFERENCE_TYPE_INFO_MAP = { + app: { + type: 'app', + key: 'app_id', + name: 'App', + }, + cloudServiceType: { + type: 'cloud_service_type', + key: 'cloud_service_type_id', + name: 'Cloud Service Type', + }, + cloudServiceQuerySet: { + type: 'cloud_service_query_set', + key: 'query_set_id', + name: 'Cloud Service Query Set', + }, + collector: { + type: 'collector', + key: 'collector_id', + name: 'Collector', + }, + costDataSource: { + type: 'cost_data_source', + key: 'data_source_id', + name: 'Cost Data Source', + }, + escalationPolicy: { + type: 'escalation_policy', + key: 'escalation_policy_id', + name: 'Escalation Policy', + }, + metric: { + type: 'metric', + key: 'metric_id', + name: 'Metric', + }, + namespace: { + type: 'namespace', + key: 'namespace_id', + name: 'Namespace', + }, + plugin: { + type: 'plugin', + key: 'plugin_id', + name: 'Plugin', + }, + projectGroup: { + type: 'project_group', + key: 'project_group_id', + name: 'Project Group', + }, + project: { + type: 'project', + key: 'project_id', + name: 'Project', + }, + publicDashboard: { + type: 'public_dashboard', + key: 'dashboard_id', + name: 'Public Dashboard', + }, + publicFolder: { + type: 'public_folder', + key: 'folder_id', + name: 'Public Folder', + }, + region: { + type: 'region', + key: 'region_code', + name: 'Region', + }, + role: { + type: 'role', + key: 'role_id', + name: 'Role', + }, + secret: { + type: 'secret', + key: 'secret_id', + name: 'Secret', + }, + serviceAccount: { + type: 'service_account', + key: 'service_account_id', + name: 'Service Account', + }, + service: { + type: 'service', + key: 'service_id', + name: 'Service', + }, + trustedAccount: { + type: 'trusted_account', + key: 'trusted_account_id', + name: 'name', + }, + userGroup: { + type: 'user_group', + key: 'user_group_id', + name: 'User Group', + }, + workspace: { + type: 'workspace', + key: 'workspace_id', + name: 'Workspace', + }, +} as const; diff --git a/apps/web/src/query/reference/_types/reference-resource-type.ts b/apps/web/src/query/reference/_types/reference-resource-type.ts new file mode 100644 index 0000000000..f0e8762fb5 --- /dev/null +++ b/apps/web/src/query/reference/_types/reference-resource-type.ts @@ -0,0 +1,4 @@ +import type { REFERENCE_TYPE_INFO_MAP } from '@/query/reference/_constants/reference-type-map-constant'; + + +export type ReferenceResourceType = typeof REFERENCE_TYPE_INFO_MAP[keyof typeof REFERENCE_TYPE_INFO_MAP]; diff --git a/apps/web/src/query/reference/_types/reference-type.ts b/apps/web/src/query/reference/_types/reference-type.ts new file mode 100644 index 0000000000..0fa2a9cd48 --- /dev/null +++ b/apps/web/src/query/reference/_types/reference-type.ts @@ -0,0 +1,28 @@ +export interface ReferenceItem> { + key?: string; + label?: string; + name?: string; + color?: string; + icon?: string; + provider?: string; + continent?: { + continent_code?: string; + continent_label?: string; + latitude?: number; + longitude?: number; + }; + latitude?: string; + longitude?: string; + data?: Data; + description?: string; + link?: string; +} + +export type ReferenceMap = Record; + +export interface ReferenceTypeInfo { + type: string; // 'project' + key: string; // project_id + name: string; // Project + referenceMap: ReferenceMap; +} diff --git a/apps/web/src/query/reference/use-all-reference-query.ts b/apps/web/src/query/reference/use-all-reference-query.ts new file mode 100644 index 0000000000..adff853a57 --- /dev/null +++ b/apps/web/src/query/reference/use-all-reference-query.ts @@ -0,0 +1,43 @@ +import { computed } from 'vue'; + +import { useQueryClient } from '@tanstack/vue-query'; + +import type { REFERENCE_TYPE_INFO_MAP } from '@/query/reference/_constants/reference-type-map-constant'; +import type { PublicDashboardReferenceMap } from '@/query/reference/use-public-dashboard-reference-query'; +import { usePublicDashboardReferenceQuery } from '@/query/reference/use-public-dashboard-reference-query'; + +export const useAllReferenceQuery = () => { + const publicDashboardQuery = usePublicDashboardReferenceQuery(); + const queryClient = useQueryClient(); + + const getters = { + publicDashboard: { + items: computed(() => publicDashboardQuery.data.value?.referenceMap ?? {}), + typeInfo: computed(() => publicDashboardQuery.data.value), + isLoading: computed(() => publicDashboardQuery.isFetching.value), + }, + // TODO: add other reference queries + }; + + const refetch = async (type: keyof typeof REFERENCE_TYPE_INFO_MAP) => { + switch (type) { + case 'publicDashboard': + await publicDashboardQuery.refetch(); break; + // TODO: add other reference queries + default: + throw new Error(`Unsupported reference type: ${type}`); + } + }; + + const refetchAll = async () => { + queryClient.invalidateQueries({ + queryKey: ['reference'], + }); + }; + + return { + getters, + refetch, + refetchAll, + }; +}; diff --git a/apps/web/src/query/reference/use-public-dashboard-reference-query.ts b/apps/web/src/query/reference/use-public-dashboard-reference-query.ts new file mode 100644 index 0000000000..fd2aa69f8b --- /dev/null +++ b/apps/web/src/query/reference/use-public-dashboard-reference-query.ts @@ -0,0 +1,56 @@ +import { usePublicDashboardApi } from '@/api-clients/dashboard/public-dashboard/composables/use-public-dashboard-api'; +import type { PublicDashboardModel } from '@/api-clients/dashboard/public-dashboard/schema/model'; +import { useReferenceQuery } from '@/query/reference/_composables/use-reference-query'; +import { useReferenceQueryKey } from '@/query/reference/_composables/use-reference-query-key'; +import type { ReferenceItem, ReferenceMap } from '@/query/reference/_types/reference-type'; + +interface PublicDashboardResourceItemData { + resourceGroup?: PublicDashboardModel['resource_group']; + projectId?: string; + folderId?: string; + shared?: boolean; + scope?: string; +} +export type PublicDashboardReferenceItem = Required, 'key'|'label'|'name'|'data'>>; +export type PublicDashboardReferenceMap = ReferenceMap; + + + +export const usePublicDashboardReferenceQuery = () => { + const queryKey = useReferenceQueryKey(); + const { publicDashboardAPI } = usePublicDashboardApi(); + + const queryFn = () => publicDashboardAPI.list({ + query: { + only: ['dashboard_id', 'name', 'project_id', 'resource_group', 'shared', 'scope', 'folder_id'], + }, + }); + + return useReferenceQuery({ + queryKey: queryKey.value.publicDashboard, + queryFn, + select: (data) => { + const referenceMap: PublicDashboardReferenceMap = {}; + data.results?.forEach((dashboard) => { + referenceMap[dashboard.dashboard_id] = { + key: dashboard.dashboard_id, + label: dashboard.name, + name: dashboard.name, + data: { + resourceGroup: dashboard.resource_group, + projectId: dashboard.project_id, + folderId: dashboard.folder_id, + shared: dashboard.shared, + scope: dashboard.scope, + }, + }; + }); + return { + type: 'public_dashboard', + key: 'dashboard_id', + name: 'Public Dashboard', + referenceMap, + }; + }, + }); +};