From 5d7aa2f54c731160a8fbecf256eae53862f9fa96 Mon Sep 17 00:00:00 2001 From: Yongtae Park Date: Mon, 24 Mar 2025 08:36:32 +0900 Subject: [PATCH 01/14] chore: update git ignore setting (cursor) Signed-off-by: samuel.park --- .gitignore | 1 + 1 file changed, 1 insertion(+) 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 From 53b4c96d2c25276ed515d313cc8b97685680c63c Mon Sep 17 00:00:00 2001 From: Yongtae Park Date: Mon, 24 Mar 2025 08:55:30 +0900 Subject: [PATCH 02/14] feat(query-key): create base resource keys (dashboard, ops-flow) Signed-off-by: samuel.park --- .../api-clients/dashboard/private-dashboard/keys.ts | 9 +++++++++ .../api-clients/dashboard/private-data-table/keys.ts | 10 ++++++++++ .../src/api-clients/dashboard/private-folder/keys.ts | 7 +++++++ .../src/api-clients/dashboard/private-widget/keys.ts | 11 +++++++++++ .../api-clients/dashboard/public-dashboard/keys.ts | 7 +++++++ .../api-clients/dashboard/public-data-table/keys.ts | 9 +++++++++ .../src/api-clients/dashboard/public-folder/keys.ts | 7 +++++++ .../src/api-clients/dashboard/public-widget/keys.ts | 11 +++++++++++ apps/web/src/api-clients/opsflow/comment/keys.ts | 7 +++++++ apps/web/src/api-clients/opsflow/event/keys.ts | 5 +++++ .../web/src/api-clients/opsflow/task-category/keys.ts | 7 +++++++ apps/web/src/api-clients/opsflow/task-type/keys.ts | 7 +++++++ apps/web/src/api-clients/opsflow/task/keys.ts | 7 +++++++ 13 files changed, 104 insertions(+) create mode 100644 apps/web/src/api-clients/dashboard/private-dashboard/keys.ts create mode 100644 apps/web/src/api-clients/dashboard/private-data-table/keys.ts create mode 100644 apps/web/src/api-clients/dashboard/private-folder/keys.ts create mode 100644 apps/web/src/api-clients/dashboard/private-widget/keys.ts create mode 100644 apps/web/src/api-clients/dashboard/public-dashboard/keys.ts create mode 100644 apps/web/src/api-clients/dashboard/public-data-table/keys.ts create mode 100644 apps/web/src/api-clients/dashboard/public-folder/keys.ts create mode 100644 apps/web/src/api-clients/dashboard/public-widget/keys.ts create mode 100644 apps/web/src/api-clients/opsflow/comment/keys.ts create mode 100644 apps/web/src/api-clients/opsflow/event/keys.ts create mode 100644 apps/web/src/api-clients/opsflow/task-category/keys.ts create mode 100644 apps/web/src/api-clients/opsflow/task-type/keys.ts create mode 100644 apps/web/src/api-clients/opsflow/task/keys.ts 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, +}; From 6d9e1fe48e20336d5dd20cc49bf661228d1961ab Mon Sep 17 00:00:00 2001 From: Yongtae Park Date: Mon, 24 Mar 2025 08:57:14 +0900 Subject: [PATCH 03/14] feat(api-key-map): create api query key map Signed-off-by: samuel.park --- .../constants/api-query-key-map-constant.ts | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 apps/web/src/api-clients/_common/constants/api-query-key-map-constant.ts 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; From ff8bc4338f7ca0a07c84f6c1cd9c76a3629e42f3 Mon Sep 17 00:00:00 2001 From: Yongtae Park Date: Mon, 24 Mar 2025 08:58:53 +0900 Subject: [PATCH 04/14] feat(api-query-key): create api query key type Signed-off-by: samuel.park --- .../src/api-clients/_common/types/query-key-type.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) 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..1af504e0fb 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,7 @@ + import type { API_DOC } from '@/api-clients/_common/constants/api-doc'; +import type { API_QUERY_KEY_MAP } from '@/api-clients/_common/constants/api-query-key-map-constant'; + /** @@ -7,3 +10,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]; From 36719f79a06a33f0ca26f8f202f77ec6583f1c62 Mon Sep 17 00:00:00 2001 From: Yongtae Park Date: Mon, 24 Mar 2025 08:59:46 +0900 Subject: [PATCH 05/14] feat(query-key-app-context): create query key app context composable Signed-off-by: samuel.park --- .../_composables/use-query-key-app-context.ts | 46 +++++++++++++++++++ apps/web/src/query/_types/query-key-type.ts | 3 ++ 2 files changed, 49 insertions(+) create mode 100644 apps/web/src/query/_composables/use-query-key-app-context.ts create mode 100644 apps/web/src/query/_types/query-key-type.ts 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..8ae47583a8 --- /dev/null +++ b/apps/web/src/query/_composables/use-query-key-app-context.ts @@ -0,0 +1,46 @@ +import { computed, reactive } from 'vue'; + +import type { QueryScope } 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'; + + + + +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/_types/query-key-type.ts b/apps/web/src/query/_types/query-key-type.ts new file mode 100644 index 0000000000..b6b277bf2f --- /dev/null +++ b/apps/web/src/query/_types/query-key-type.ts @@ -0,0 +1,3 @@ +export type QueryKeyArray = unknown[]; + +export type QueryScope = 'service' | 'reference'; From 7a50368a03d0337bce3c24b1f1ca8794786dab5b Mon Sep 17 00:00:00 2001 From: Yongtae Park Date: Mon, 24 Mar 2025 09:00:40 +0900 Subject: [PATCH 06/14] feat(query-key): create new service query key composable (_useAPIQueryKey) Signed-off-by: samuel.park --- .../_common/composables/use-api-query-key.ts | 96 +++++++++++++++---- .../query/_helpers/immutable-key-helper.ts | 16 ++++ 2 files changed, 92 insertions(+), 20 deletions(-) create mode 100644 apps/web/src/query/_helpers/immutable-key-helper.ts 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..64f84ea4b0 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,87 @@ -import { computed, reactive } from 'vue'; +import type { ComputedRef } from 'vue'; +import { reactive, computed } from 'vue'; + +import { API_QUERY_KEY_MAP } from '@/api-clients/_common/constants/api-query-key-map-constant'; 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/_helpers/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); +}; + + + +// TODO: Deprecate this interface GlobalQueryParams { workspaceId?: string; isAdminMode?: boolean; 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..f1372f94b1 --- /dev/null +++ b/apps/web/src/query/_helpers/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; +}; From b15bb035906e997f7f99e7dede2859f6c0e739c8 Mon Sep 17 00:00:00 2001 From: Yongtae Park Date: Mon, 24 Mar 2025 09:01:08 +0900 Subject: [PATCH 07/14] feat(query-invalidator): create service query global invalidator Signed-off-by: samuel.park --- .../_common/helpers/service-query-invalidator-helper.ts | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 apps/web/src/api-clients/_common/helpers/service-query-invalidator-helper.ts 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..2a26826df3 --- /dev/null +++ b/apps/web/src/api-clients/_common/helpers/service-query-invalidator-helper.ts @@ -0,0 +1,7 @@ +import { queryClient } from '@/query'; + +export const invalidateServiceQuery = async (mode: 'admin' | 'workspace') => { + await queryClient.invalidateQueries({ + queryKey: ['service', mode], + }); +}; From 0701fdfb5194375549d54eab65201cdf99a5f143 Mon Sep 17 00:00:00 2001 From: Yongtae Park Date: Mon, 24 Mar 2025 09:01:44 +0900 Subject: [PATCH 08/14] chore: refactor scoped query composable Signed-off-by: samuel.park --- .../_common/composables/use-scoped-query.ts | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) 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({ From c0e6508f5a6c7d85a05ebac45a110b19c5937ac1 Mon Sep 17 00:00:00 2001 From: Yongtae Park Date: Mon, 24 Mar 2025 09:02:06 +0900 Subject: [PATCH 09/14] chore: create global query client instance Signed-off-by: samuel.park --- apps/web/src/main.ts | 5 ++++- apps/web/src/query/index.ts | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 apps/web/src/query/index.ts diff --git a/apps/web/src/main.ts b/apps/web/src/main.ts index 3d8cc3687e..0e2675fecc 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/index'; 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 }); + directive(Vue); 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(); From a9cda64e8662bd13658335dcec6caf3ecf779b0b Mon Sep 17 00:00:00 2001 From: Yongtae Park Date: Mon, 24 Mar 2025 09:34:50 +0900 Subject: [PATCH 10/14] draft: reference query --- .../_composables/use-reference-query-key.ts | 17 +++ .../_composables/use-reference-query-sync.ts | 71 ++++++++++++ .../_composables/use-reference-query.ts | 39 +++++++ .../_constants/reference-type-map-constant.ts | 107 ++++++++++++++++++ .../_types/reference-resource-type.ts | 4 + .../query/reference/_types/reference-type.ts | 28 +++++ .../reference/use-all-reference-query.ts | 43 +++++++ .../use-public-dashboard-reference-query.ts | 56 +++++++++ 8 files changed, 365 insertions(+) create mode 100644 apps/web/src/query/reference/_composables/use-reference-query-key.ts create mode 100644 apps/web/src/query/reference/_composables/use-reference-query-sync.ts create mode 100644 apps/web/src/query/reference/_composables/use-reference-query.ts create mode 100644 apps/web/src/query/reference/_constants/reference-type-map-constant.ts create mode 100644 apps/web/src/query/reference/_types/reference-resource-type.ts create mode 100644 apps/web/src/query/reference/_types/reference-type.ts create mode 100644 apps/web/src/query/reference/use-all-reference-query.ts create mode 100644 apps/web/src/query/reference/use-public-dashboard-reference-query.ts 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..87502e1c1d --- /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('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..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, + }; + }, + }); +}; From 198a47b78fe696320c900253b92a63b8ac72fcca Mon Sep 17 00:00:00 2001 From: Yongtae Park Date: Tue, 25 Mar 2025 15:31:04 +0900 Subject: [PATCH 11/14] fix(query-client): separate query client (reference, service) Signed-off-by: samuel.park --- .../_common/helpers/service-query-invalidator-helper.ts | 4 ++-- apps/web/src/main.ts | 4 ++-- apps/web/src/query/clients.ts | 5 +++++ apps/web/src/query/index.ts | 3 --- 4 files changed, 9 insertions(+), 7 deletions(-) create mode 100644 apps/web/src/query/clients.ts delete mode 100644 apps/web/src/query/index.ts 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 index 2a26826df3..d249d989af 100644 --- 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 @@ -1,7 +1,7 @@ -import { queryClient } from '@/query'; +import { apiQueryClient } from '@/query/clients'; export const invalidateServiceQuery = async (mode: 'admin' | 'workspace') => { - await queryClient.invalidateQueries({ + await apiQueryClient.invalidateQueries({ queryKey: ['service', mode], }); }; diff --git a/apps/web/src/main.ts b/apps/web/src/main.ts index 0e2675fecc..5a56871cdf 100644 --- a/apps/web/src/main.ts +++ b/apps/web/src/main.ts @@ -10,7 +10,7 @@ import VTooltip from 'v-tooltip'; import SpaceDesignSystem from '@cloudforet/mirinae'; import directive from '@/directives'; -import { queryClient } from '@/query/index'; +import { apiQueryClient } from '@/query/clients'; import { SpaceRouter } from '@/router'; import { i18n } from '@/translations'; @@ -32,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, { queryClient }); +Vue.use(VueQueryPlugin, { queryClient: apiQueryClient }); directive(Vue); 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/index.ts b/apps/web/src/query/index.ts deleted file mode 100644 index 5e6f978ebc..0000000000 --- a/apps/web/src/query/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { QueryClient } from '@tanstack/vue-query'; - -export const queryClient = new QueryClient(); From 388577527cc495d5c886792933adf8f1c960f917 Mon Sep 17 00:00:00 2001 From: Yongtae Park Date: Tue, 25 Mar 2025 15:33:06 +0900 Subject: [PATCH 12/14] fix(query-key): move directory & remove query scope Signed-off-by: samuel.park --- .../_common/composables/use-api-query-key.ts | 112 +++++++++--------- .../_common/types/query-key-type.ts | 22 ---- .../_composables/use-query-key-app-context.ts | 8 +- apps/web/src/query/_types/query-key-type.ts | 12 ++ .../_composables/use-reference-query-key.ts | 2 +- 5 files changed, 71 insertions(+), 85 deletions(-) delete mode 100644 apps/web/src/api-clients/_common/types/query-key-type.ts 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 64f84ea4b0..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,83 +1,77 @@ -import type { ComputedRef } from 'vue'; import { reactive, computed } from 'vue'; -import { API_QUERY_KEY_MAP } from '@/api-clients/_common/constants/api-query-key-map-constant'; import type { - 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/_helpers/immutable-key-helper'; -import type { QueryKeyArray } from '@/query/_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'; +// type QueryKeyArrayWithDep = QueryKeyArray & { +// addDep: (deps: Record) => QueryKeyArray; +// }; +// type ExtractParams = T extends (params: infer P) => any ? P : never; -type QueryKeyArrayWithDep = QueryKeyArray & { - addDep: (deps: Record) => QueryKeyArray; -}; -type ExtractParams = T extends (params: infer P) => any ? P : never; +// type VerbFunction = { +// (params?: ExtractParams): QueryKeyArrayWithDep; +// }; -type VerbFunction = { - (params?: ExtractParams): QueryKeyArrayWithDep; -}; +// type MapVerbToReturnType = T extends (params: any) => any +// ? VerbFunction +// : never; -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 UseAPIQueryResult = { - [S in APIQueryKeyMapService]: { - [R in APIQueryKeyMapResource]: { - [V in APIQueryKeyMapVerb]: MapVerbToReturnType<(typeof API_QUERY_KEY_MAP)[S][R][V]>; - }; - }; -}; +// type APIQueryKeyValue = (params?: Record) => QueryKeyArray; -type APIQueryKeyValue = (params?: Record) => QueryKeyArray; +// export const _useAPIQueryKey = (): ComputedRef => { +// const queryKeyAppContext = useQueryKeyAppContext('service'); +// const globalContext = computed(() => queryKeyAppContext.value); -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 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; - 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), +// ]; - (queryKey as QueryKeyArrayWithDep).addDep = (deps: Record): QueryKeyArray => [ - ...globalContext.value, - ...staticKey, - ...createImmutableObject(baseKey.value), - createImmutableObject(deps), - ]; +// return queryKey as QueryKeyArrayWithDep; +// }; - return queryKey as QueryKeyArrayWithDep; - }; +// verbResult[verb] = verbFunction; +// return verbResult; +// }, {}); +// return resourceResult; +// }, {}); +// return result; +// }, {} as Record); - verbResult[verb] = verbFunction; - return verbResult; - }, {}); - return resourceResult; - }, {}); - return result; - }, {} as Record); - - return computed(() => apiStructure as UseAPIQueryResult); -}; +// return computed(() => apiStructure as UseAPIQueryResult); +// }; @@ -108,3 +102,7 @@ export const useAPIQueryKey = , return computed(() => [service, resource, verb, { ...globalQueryParams }]); }; + + + + 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 deleted file mode 100644 index 1af504e0fb..0000000000 --- a/apps/web/src/api-clients/_common/types/query-key-type.ts +++ /dev/null @@ -1,22 +0,0 @@ - -import type { API_DOC } from '@/api-clients/_common/constants/api-doc'; -import type { API_QUERY_KEY_MAP } from '@/api-clients/_common/constants/api-query-key-map-constant'; - - - -/** - * Extracts all possible keys for `{service-name}`, `{resource-name}`, and `{verb}` - */ -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/query/_composables/use-query-key-app-context.ts b/apps/web/src/query/_composables/use-query-key-app-context.ts index 8ae47583a8..0077545a53 100644 --- a/apps/web/src/query/_composables/use-query-key-app-context.ts +++ b/apps/web/src/query/_composables/use-query-key-app-context.ts @@ -1,7 +1,5 @@ import { computed, reactive } from 'vue'; -import type { QueryScope } 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'; @@ -21,7 +19,7 @@ interface WorkspaceModeState { type QueryKeyState = AdminModeState | WorkspaceModeState; -export const useQueryKeyAppContext = (queryScope: QueryScope = 'service') => { +export const useQueryKeyAppContext = () => { const appContextStore = useAppContextStore(); const userWorkspaceStore = useUserWorkspaceStore(); @@ -40,7 +38,7 @@ export const useQueryKeyAppContext = (queryScope: QueryScope = 'service') => { }; return state.isAdminMode - ? [queryScope, 'admin'] - : [queryScope, 'workspace', state.workspaceId]; + ? ['admin'] + : ['workspace', state.workspaceId]; }); }; diff --git a/apps/web/src/query/_types/query-key-type.ts b/apps/web/src/query/_types/query-key-type.ts index b6b277bf2f..1a647835df 100644 --- a/apps/web/src/query/_types/query-key-type.ts +++ b/apps/web/src/query/_types/query-key-type.ts @@ -1,3 +1,15 @@ +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}` + */ +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; 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 index 87502e1c1d..1f07141386 100644 --- a/apps/web/src/query/reference/_composables/use-reference-query-key.ts +++ b/apps/web/src/query/reference/_composables/use-reference-query-key.ts @@ -8,7 +8,7 @@ import { REFERENCE_TYPE_INFO_MAP } from '@/query/reference/_constants/reference- type UseReferenceQueryKeyResult = Record; export const useReferenceQueryKey = (): ComputedRef => { - const queryKeyAppContext = useQueryKeyAppContext('reference'); + const queryKeyAppContext = useQueryKeyAppContext(); return computed(() => Object.entries(REFERENCE_TYPE_INFO_MAP).reduce((acc, [key, value]) => ({ ...acc, From 8a3fb964171109ee27472ddd793022b8f8b41bad Mon Sep 17 00:00:00 2001 From: Yongtae Park Date: Tue, 25 Mar 2025 15:33:29 +0900 Subject: [PATCH 13/14] feat(api-query): create new api query Signed-off-by: samuel.park --- .../query/_composables/use-api-query-key.ts | 47 +++++++++++++++++++ .../query/_helpers/immutable-key-helper.ts | 6 +-- 2 files changed, 50 insertions(+), 3 deletions(-) create mode 100644 apps/web/src/query/_composables/use-api-query-key.ts 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/_helpers/immutable-key-helper.ts b/apps/web/src/query/_helpers/immutable-key-helper.ts index f1372f94b1..9de930582d 100644 --- a/apps/web/src/query/_helpers/immutable-key-helper.ts +++ b/apps/web/src/query/_helpers/immutable-key-helper.ts @@ -1,15 +1,15 @@ -export const createImmutableObject = >(obj: T): T => { +export const createImmutableObjectKeyItem = >(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; + return obj.map((item) => createImmutableObjectKeyItem(item)) as unknown as T; } const immutableObj = Object.entries(obj).reduce((acc, [key, value]) => ({ ...acc, - [key]: createImmutableObject(value), + [key]: createImmutableObjectKeyItem(value), }), {}); return Object.freeze(immutableObj) as T; From 2685921bbcd1350fd4a269da8b3f92681397f905 Mon Sep 17 00:00:00 2001 From: Yongtae Park Date: Tue, 25 Mar 2025 15:33:51 +0900 Subject: [PATCH 14/14] draft: create reference query --- .../use-reference-advance-query.ts | 164 ++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 apps/web/src/query/reference/_composables/use-reference-advance-query.ts 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, + }; +};