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..1eb55742ab 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,30 +1,86 @@ -import { computed, reactive } from 'vue'; +import type { ComputedRef } from 'vue'; +import { reactive, computed } from 'vue'; +import type { QueryKey } from '@tanstack/vue-query'; + +import { API_QUERY_KEY_MAP } from '@/api-clients/_common/constants/api-query-key-map'; import type { - ResourceName, ServiceName, Verb, + APIQueryKeyMapService, APIQueryKeyMapResource, APIQueryKeyMapVerb, ServiceName, ResourceName, Verb, } from '@/api-clients/_common/types/query-key-type'; +import { useQueryKeyAppContext } from '@/query/_composables/use-query-key-app-context'; +import { createImmutableObject } from '@/query/_helper/immutable-key-helper'; +import type { QueryKeyArray } 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); +}; + + + interface GlobalQueryParams { workspaceId?: string; @@ -35,7 +91,7 @@ export const useAPIQueryKey = , resource: R, verb: V, additionalGlobalParams?: Partial, -) => { +): ComputedRef => { const appContextStore = useAppContextStore(); const userWorkspaceStore = useUserWorkspaceStore(); @@ -50,5 +106,5 @@ export const useAPIQueryKey = , ...additionalGlobalParams, }); - return computed(() => [service, resource, verb, { ...globalQueryParams }]); + 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.ts b/apps/web/src/api-clients/_common/constants/api-query-key-map.ts new file mode 100644 index 0000000000..b7ff3c05a9 --- /dev/null +++ b/apps/web/src/api-clients/_common/constants/api-query-key-map.ts @@ -0,0 +1,34 @@ +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/constants/query-key-constant.ts b/apps/web/src/api-clients/_common/constants/query-key-constant.ts new file mode 100644 index 0000000000..07acce3fe0 --- /dev/null +++ b/apps/web/src/api-clients/_common/constants/query-key-constant.ts @@ -0,0 +1 @@ +export const SERVICE_PREFIX = 'service' as const; diff --git a/apps/web/src/api-clients/_common/helpers/service-query-invalidation-helper.ts b/apps/web/src/api-clients/_common/helpers/service-query-invalidation-helper.ts new file mode 100644 index 0000000000..2a26826df3 --- /dev/null +++ b/apps/web/src/api-clients/_common/helpers/service-query-invalidation-helper.ts @@ -0,0 +1,7 @@ +import { queryClient } from '@/query'; + +export const invalidateServiceQuery = async (mode: 'admin' | 'workspace') => { + await queryClient.invalidateQueries({ + queryKey: ['service', mode], + }); +}; diff --git a/apps/web/src/api-clients/_common/types/query-key-type.ts b/apps/web/src/api-clients/_common/types/query-key-type.ts index b8430ca461..c1abcbb528 100644 --- a/apps/web/src/api-clients/_common/types/query-key-type.ts +++ b/apps/web/src/api-clients/_common/types/query-key-type.ts @@ -1,4 +1,8 @@ import type { API_DOC } from '@/api-clients/_common/constants/api-doc'; +// import type { QueryContext } from '@/query/_types/query-key-type'; + +import type { API_QUERY_KEY_MAP } from '../constants/api-query-key-map'; + /** @@ -7,3 +11,13 @@ import type { API_DOC } from '@/api-clients/_common/constants/api-doc'; export type ServiceName = keyof typeof API_DOC; export type ResourceName = keyof (typeof API_DOC)[S]; export type Verb> = Extract<(typeof API_DOC)[S][R], string[]>[number] | string; + +export type ServiceQueryKey, V extends Verb> = [ + S, + R, + V, +]; + +export type APIQueryKeyMapService = keyof typeof API_QUERY_KEY_MAP; +export type APIQueryKeyMapResource = keyof (typeof API_QUERY_KEY_MAP)[S]; +export type APIQueryKeyMapVerb> = keyof (typeof API_QUERY_KEY_MAP)[S][R]; 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..f60d01e88d --- /dev/null +++ b/apps/web/src/api-clients/dashboard/private-data-table/keys.ts @@ -0,0 +1,9 @@ +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/composables/use-public-dashboard-api.ts b/apps/web/src/api-clients/dashboard/public-dashboard/composables/use-public-dashboard-api.ts index e2969a6483..e243d5d0ba 100644 --- a/apps/web/src/api-clients/dashboard/public-dashboard/composables/use-public-dashboard-api.ts +++ b/apps/web/src/api-clients/dashboard/public-dashboard/composables/use-public-dashboard-api.ts @@ -15,6 +15,7 @@ import type { PublicDashboardShareParameters } from '@/api-clients/dashboard/pub import type { PublicDashboardUnshareParameters } from '@/api-clients/dashboard/public-dashboard/schema/api-verbs/unshare'; import type { PublicDashboardUpdateParameters } from '@/api-clients/dashboard/public-dashboard/schema/api-verbs/update'; import type { PublicDashboardModel } from '@/api-clients/dashboard/public-dashboard/schema/model'; +import { useReferenceQuerySync } from '@/query/reference/_composables/use-reference-query-sync'; interface UsePublicDashboardApiReturn { publicDashboardGetQueryKey: ComputedRef; @@ -34,13 +35,14 @@ interface UsePublicDashboardApiReturn { export const usePublicDashboardApi = (): UsePublicDashboardApiReturn => { const publicDashboardGetQueryKey = useAPIQueryKey('dashboard', 'public-dashboard', 'get'); const publicDashboardListQueryKey = useAPIQueryKey('dashboard', 'public-dashboard', 'list'); + const { withReferenceUpdate, withReferenceRefresh } = useReferenceQuerySync('publicDashboard'); const actions = { async create(params: PublicDashboardCreateParameters) { - return SpaceConnector.clientV2.dashboard.publicDashboard.create(params); + return withReferenceUpdate(() => SpaceConnector.clientV2.dashboard.publicDashboard.create(params)); }, async update(params: PublicDashboardUpdateParameters) { - return SpaceConnector.clientV2.dashboard.publicDashboard.update(params); + return withReferenceUpdate(() => SpaceConnector.clientV2.dashboard.publicDashboard.update(params)); }, async changeFolder(params: PublicDashboardChangeFolderParameters) { return SpaceConnector.clientV2.dashboard.publicDashboard.changeFolder(params); @@ -52,7 +54,7 @@ export const usePublicDashboardApi = (): UsePublicDashboardApiReturn => { return SpaceConnector.clientV2.dashboard.publicDashboard.unshare(params); }, async delete(params: PublicDashboardDeleteParameters) { - return SpaceConnector.clientV2.dashboard.publicDashboard.delete(params); + return withReferenceRefresh(() => SpaceConnector.clientV2.dashboard.publicDashboard.delete(params)); }, async get(params: PublicDashboardGetParameters) { return SpaceConnector.clientV2.dashboard.publicDashboard.get(params); 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..80878c2455 --- /dev/null +++ b/apps/web/src/api-clients/opsflow/comment/keys.ts @@ -0,0 +1,8 @@ +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..f3dd352a10 --- /dev/null +++ b/apps/web/src/api-clients/opsflow/event/keys.ts @@ -0,0 +1,6 @@ +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..624f6ed709 --- /dev/null +++ b/apps/web/src/api-clients/opsflow/task-category/keys.ts @@ -0,0 +1,8 @@ +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..f5c5473eeb --- /dev/null +++ b/apps/web/src/api-clients/opsflow/task-type/keys.ts @@ -0,0 +1,8 @@ +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..201d6996fb --- /dev/null +++ b/apps/web/src/api-clients/opsflow/task/keys.ts @@ -0,0 +1,8 @@ +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/common/modules/navigations/top-bar/modules/top-bar-toolset/modules/top-bar-admin-toggle-button/TopBarAdminToggleButton.vue b/apps/web/src/common/modules/navigations/top-bar/modules/top-bar-toolset/modules/top-bar-admin-toggle-button/TopBarAdminToggleButton.vue index 1e47de19ed..83f8bcc3be 100644 --- a/apps/web/src/common/modules/navigations/top-bar/modules/top-bar-toolset/modules/top-bar-admin-toggle-button/TopBarAdminToggleButton.vue +++ b/apps/web/src/common/modules/navigations/top-bar/modules/top-bar-toolset/modules/top-bar-admin-toggle-button/TopBarAdminToggleButton.vue @@ -4,6 +4,7 @@ import { useRouter } from 'vue-router/composables'; import { throttle } from 'lodash'; +import { invalidateServiceQuery } from '@/api-clients/_common/helpers/service-query-invalidation-helper'; import type { WorkspaceModel } from '@/api-clients/identity/workspace/schema/model'; import { i18n } from '@/translations'; @@ -16,6 +17,7 @@ import { getLastAccessedWorkspaceId } from '@/lib/site-initializer/last-accessed import { LANDING_ROUTE } from '@/services/landing/routes/route-constant'; + const appContextStore = useAppContextStore(); const userWorkspaceStore = useUserWorkspaceStore(); const workspaceStoreGetters = userWorkspaceStore.getters; @@ -34,6 +36,7 @@ const handleToggleAdminMode = throttle(async () => { return; } appContextStore.setGlobalGrantLoading(true); + await invalidateServiceQuery(state.isAdminMode ? 'admin' : 'workspace'); if (state.isAdminMode) { await userWorkspaceStore.load(); if (state.workspaceList.length === 0) { diff --git a/apps/web/src/main.ts b/apps/web/src/main.ts index 3d8cc3687e..6ef2d9a2fc 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 { queryClient } from '@/query'; import { SpaceRouter } from '@/router'; import { i18n } from '@/translations'; @@ -17,6 +18,7 @@ import { pinia } from '@/store/pinia'; import { siteInit } from '@/lib/site-initializer'; + import App from './App.vue'; import '@/styles/style.pcss'; @@ -30,7 +32,7 @@ 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 }); directive(Vue); 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..6597ef191d --- /dev/null +++ b/apps/web/src/query/_composables/use-query-key-app-context.ts @@ -0,0 +1,114 @@ +import type { ComputedRef } from 'vue'; +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 for global query parameters used across API requests. + * These parameters are automatically included in API and reference query keys. + */ +export type QueryScope = 'service' | 'reference'; + +export interface QueryKeyContextParams { + isAdminMode?: boolean; + workspaceId?: string; + scope: QueryScope; +} + +export interface QueryContext { + mode: 'admin' | 'workspace'; + workspaceId?: string; + scope: QueryScope; +} + +/** + * Composable that manages global query parameters with reactive state. + * This hook centralizes the management of workspace and admin mode parameters + * that are commonly used across different API queries. + * + * @param queryKeyOptions - Optional query key options to merge with default params + * @returns A computed reference containing the combined global query parameters as a QueryContext object + * + * ### Example Usage: + * ```ts + * const queryContext = useQueryKeyContext({ context: 'service' }); + * // Access params + * const { mode, workspaceId, context } = queryContext.value; + * + * // With additional params + * const params = useQueryKeyContext({ + * context: 'service', + * isAdminMode: true, + * workspaceId: 'custom-id' + * }); + * ``` + * + * ### Features: + * - **Structured Context**: Returns a QueryContext object with mode, workspaceId, and context + * - **Reactive State**: Automatically updates when workspace or admin mode changes + * - **Type Safety**: Ensures context is always provided with valid values + */ +export const useQueryKeyContext = (queryKeyOptions: QueryKeyContextParams): ComputedRef => { + const appContextStore = useAppContextStore(); + const userWorkspaceStore = useUserWorkspaceStore(); + + const _state = reactive({ + isAdminMode: computed(() => appContextStore.getters.isAdminMode), + currentWorkspaceId: computed(() => userWorkspaceStore.getters.currentWorkspaceId), + }); + + return computed(() => { + const { + isAdminMode, workspaceId: _workspaceId, scope, + } = queryKeyOptions || {}; + const mode = (isAdminMode || _state.isAdminMode) ? 'admin' : 'workspace'; + const workspaceId = _workspaceId || _state.currentWorkspaceId; + + return { + mode, + workspaceId, + scope, + }; + }); +}; + + +interface AdminModeState { + isAdminMode: true; + workspaceId?: undefined; +} + +interface WorkspaceModeState { + isAdminMode: false; + workspaceId: string; +} + +type QueryKeyState = AdminModeState | WorkspaceModeState; + + +export const useQueryKeyAppContext = (queryScope: QueryScope = 'service') => { + 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 + ? [queryScope, 'admin'] + : [queryScope, 'workspace', state.workspaceId]; + }); +}; + diff --git a/apps/web/src/query/_composables/use-reference-query.ts b/apps/web/src/query/_composables/use-reference-query.ts new file mode 100644 index 0000000000..8f5484bb51 --- /dev/null +++ b/apps/web/src/query/_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/_helper/immutable-key-helper.ts b/apps/web/src/query/_helper/immutable-key-helper.ts new file mode 100644 index 0000000000..f1372f94b1 --- /dev/null +++ b/apps/web/src/query/_helper/immutable-key-helper.ts @@ -0,0 +1,16 @@ +export const createImmutableObject = >(obj: T): T => { + if (obj === null || typeof obj !== 'object') { + return obj; + } + + if (Array.isArray(obj)) { + return obj.map((item) => createImmutableObject(item)) as unknown as T; + } + + const immutableObj = Object.entries(obj).reduce((acc, [key, value]) => ({ + ...acc, + [key]: createImmutableObject(value), + }), {}); + + return Object.freeze(immutableObj) as T; +}; diff --git a/apps/web/src/query/_types/query-key-type.ts b/apps/web/src/query/_types/query-key-type.ts new file mode 100644 index 0000000000..9f3caa44d1 --- /dev/null +++ b/apps/web/src/query/_types/query-key-type.ts @@ -0,0 +1,10 @@ + +export interface QueryContext { + mode: 'admin' | 'workspace', + workspaceId: string | undefined, + context: 'service' | 'reference', +} + + +export type QueryKeyArray = unknown[]; + diff --git a/apps/web/src/query/index.ts b/apps/web/src/query/index.ts new file mode 100644 index 0000000000..5e6f978ebc --- /dev/null +++ b/apps/web/src/query/index.ts @@ -0,0 +1,3 @@ +import { QueryClient } from '@tanstack/vue-query'; + +export const queryClient = new QueryClient(); 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..c67d8e6f0f --- /dev/null +++ b/apps/web/src/query/reference/_composables/use-reference-query-key.ts @@ -0,0 +1,18 @@ +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'; + + +type UseReferenceQueryKeyResult = Record; + +export const useReferenceQueryKey = (): ComputedRef => { + const queryKeyAppContext = useQueryKeyAppContext('reference'); + + 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..b976b48781 --- /dev/null +++ b/apps/web/src/query/reference/_composables/use-reference-query-sync.ts @@ -0,0 +1,77 @@ +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'; + + +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/_constants/reference-type-map.ts b/apps/web/src/query/reference/_constants/reference-type-map.ts new file mode 100644 index 0000000000..a70a252c89 --- /dev/null +++ b/apps/web/src/query/reference/_constants/reference-type-map.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/_helper/reference-query-invalidation-helper.ts b/apps/web/src/query/reference/_helper/reference-query-invalidation-helper.ts new file mode 100644 index 0000000000..919683a837 --- /dev/null +++ b/apps/web/src/query/reference/_helper/reference-query-invalidation-helper.ts @@ -0,0 +1,7 @@ +import { queryClient } from '@/query'; + +export const invalidateReferenceQuery = async (mode: 'admin' | 'workspace') => { + await queryClient.invalidateQueries({ + queryKey: ['reference', mode], + }); +}; diff --git a/apps/web/src/query/reference/_types/reference-query-key-type.ts b/apps/web/src/query/reference/_types/reference-query-key-type.ts new file mode 100644 index 0000000000..1b827fafae --- /dev/null +++ b/apps/web/src/query/reference/_types/reference-query-key-type.ts @@ -0,0 +1,8 @@ +import type { QueryContext } from '@/query/_types/query-key-type'; +import type { ReferenceResourceType } from '@/query/reference/_types/reference-resource-type'; + + +export type ReferenceQueryKey = [ + QueryContext, + ReferenceResourceType +]; diff --git a/apps/web/src/query/reference/_types/reference-query-type.ts b/apps/web/src/query/reference/_types/reference-query-type.ts new file mode 100644 index 0000000000..0fa2a9cd48 --- /dev/null +++ b/apps/web/src/query/reference/_types/reference-query-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/_types/reference-resource-type.ts b/apps/web/src/query/reference/_types/reference-resource-type.ts new file mode 100644 index 0000000000..b5fe29a379 --- /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'; + + +export type ReferenceResourceType = typeof REFERENCE_TYPE_INFO_MAP[keyof typeof REFERENCE_TYPE_INFO_MAP]; 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..4e139e8e81 --- /dev/null +++ b/apps/web/src/query/reference/use-all-reference-query.ts @@ -0,0 +1,44 @@ +import { computed } from 'vue'; + +import { useQueryClient } from '@tanstack/vue-query'; + +import type { PublicDashboardReferenceMap } from '@/query/reference/use-public-dashboard-reference-query'; +import { usePublicDashboardReferenceQuery } from '@/query/reference/use-public-dashboard-reference-query'; + +import type { REFERENCE_TYPE_INFO_MAP } from './_constants/reference-type-map'; + +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..c556a0996b --- /dev/null +++ b/apps/web/src/query/reference/use-public-dashboard-reference-query.ts @@ -0,0 +1,58 @@ +import type { ComputedRef } from 'vue'; + +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/_composables/use-reference-query'; +import { useReferenceQueryKey } from '@/query/reference/_composables/use-reference-query-key'; +import type { ReferenceItem, ReferenceMap } from '@/query/reference/_types/reference-query-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, + }; + }, + }); +};