From 83b22782d4708ec85e27169bc7edc758fe7f46db Mon Sep 17 00:00:00 2001 From: Yongtae Park Date: Tue, 25 Mar 2025 15:39:44 +0900 Subject: [PATCH 01/17] chore(api-doc): edit api-doc file name Signed-off-by: samuel.park --- apps/web/cli/api-doc-gen.js | 2 +- apps/web/package.json | 2 +- .../_common/constants/{api-doc.ts => api-doc-constant.ts} | 0 apps/web/src/api-clients/_common/types/query-key-type.ts | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename apps/web/src/api-clients/_common/constants/{api-doc.ts => api-doc-constant.ts} (100%) diff --git a/apps/web/cli/api-doc-gen.js b/apps/web/cli/api-doc-gen.js index ffe1e79572..feeb29b90d 100644 --- a/apps/web/cli/api-doc-gen.js +++ b/apps/web/cli/api-doc-gen.js @@ -2,7 +2,7 @@ import fs from 'fs'; import path from 'path'; const API_DIR = './src/api-clients'; -const OUTPUT_PATH = './src/api-clients/_common/constants/api-doc.ts'; +const OUTPUT_PATH = './src/api-clients/_common/constants/api-doc-constant.ts'; const generateAPIDocumentation = (basePath) => { diff --git a/apps/web/package.json b/apps/web/package.json index 2b9606448b..59600c142a 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -19,7 +19,7 @@ "stylelint:fix": "stylelint --fix \"src/**/*.{css,vue,pcss,scss}\"", "format": "npm run eslint:fix && npm run stylelint:fix", "postcss": "node build/reset-css.js", - "api-doc": "node ./cli/api-doc-gen.js && npx eslint ./src/api-clients/_common/constants/api-doc.ts --fix" + "api-doc": "node ./cli/api-doc-gen.js && npx eslint ./src/api-clients/_common/constants/api-doc-constant.ts --fix" }, "main": "dist/cloudforet-component.common.js", "dependencies": { diff --git a/apps/web/src/api-clients/_common/constants/api-doc.ts b/apps/web/src/api-clients/_common/constants/api-doc-constant.ts similarity index 100% rename from apps/web/src/api-clients/_common/constants/api-doc.ts rename to apps/web/src/api-clients/_common/constants/api-doc-constant.ts 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..fadaec94d7 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,4 @@ -import type { API_DOC } from '@/api-clients/_common/constants/api-doc'; +import type { API_DOC } from '@/api-clients/_common/constants/api-doc-constant'; /** From e3b6ee6370acdc5c1fd9dd2931daa471f7ced2d5 Mon Sep 17 00:00:00 2001 From: Yongtae Park Date: Tue, 25 Mar 2025 16:37:37 +0900 Subject: [PATCH 02/17] feat(query-client): separate query client (reference/service) Signed-off-by: samuel.park --- apps/web/src/main.ts | 4 ++-- apps/web/src/query/clients.ts | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 apps/web/src/query/clients.ts diff --git a/apps/web/src/main.ts b/apps/web/src/main.ts index 3d8cc3687e..768eb7729a 100644 --- a/apps/web/src/main.ts +++ b/apps/web/src/main.ts @@ -10,6 +10,7 @@ import VTooltip from 'v-tooltip'; import SpaceDesignSystem from '@cloudforet/mirinae'; import directive from '@/directives'; +import { apiQueryClient } from '@/query/clients'; import { SpaceRouter } from '@/router'; import { i18n } from '@/translations'; @@ -24,13 +25,12 @@ import '@/styles/style.pcss'; import '@cloudforet/mirinae/css/light-style.css'; import '@cloudforet/mirinae/dist/style.css'; - /** ********** SET VUE PLUGINS ************** */ 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, { defaultQueryClient: 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(); From bcef26e22ae8cf2622f19a892451a6aa8de882db Mon Sep 17 00:00:00 2001 From: Yongtae Park Date: Tue, 25 Mar 2025 16:38:15 +0900 Subject: [PATCH 03/17] feat(query-key): create app context query key composable Signed-off-by: samuel.park --- apps/web/src/query/_types/query-key-type.ts | 15 +++++++ .../composables/use-app-context-query-key.ts | 44 +++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 apps/web/src/query/_types/query-key-type.ts create mode 100644 apps/web/src/query/composables/use-app-context-query-key.ts 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..88695444ed --- /dev/null +++ b/apps/web/src/query/_types/query-key-type.ts @@ -0,0 +1,15 @@ +import type { API_DOC } from '@/api-clients/_common/constants/api-doc-constant'; + + +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/composables/use-app-context-query-key.ts b/apps/web/src/query/composables/use-app-context-query-key.ts new file mode 100644 index 0000000000..0077545a53 --- /dev/null +++ b/apps/web/src/query/composables/use-app-context-query-key.ts @@ -0,0 +1,44 @@ +import { computed, reactive } from 'vue'; + +import { useAppContextStore } from '@/store/app-context/app-context-store'; +import { useUserWorkspaceStore } from '@/store/app-context/workspace/user-workspace-store'; + + + + +interface AdminModeState { + isAdminMode: true; + workspaceId?: undefined; +} + +interface WorkspaceModeState { + isAdminMode: false; + workspaceId: string; +} + +type QueryKeyState = AdminModeState | WorkspaceModeState; + + +export const useQueryKeyAppContext = () => { + const appContextStore = useAppContextStore(); + const userWorkspaceStore = useUserWorkspaceStore(); + + const _state = reactive({ + isAdminMode: computed(() => appContextStore.getters.isAdminMode), + workspaceId: computed(() => userWorkspaceStore.getters.currentWorkspaceId), + }); + + return computed(() => { + const state: QueryKeyState = _state.isAdminMode + ? { isAdminMode: true } + : { + isAdminMode: false, + // workspaceId: _state.workspaceId! + workspaceId: _state.workspaceId ?? '', + }; + + return state.isAdminMode + ? ['admin'] + : ['workspace', state.workspaceId]; + }); +}; From 0db316b2e241a7fa36156ac1778dc4654832f300 Mon Sep 17 00:00:00 2001 From: Yongtae Park Date: Tue, 25 Mar 2025 16:38:40 +0900 Subject: [PATCH 04/17] feat(query-key): create new api query key composable Signed-off-by: samuel.park --- .../_helpers/immutable-query-key-helper.ts | 16 ++++ .../query/composables/use-api-query-key.ts | 85 +++++++++++++++++++ 2 files changed, 101 insertions(+) create mode 100644 apps/web/src/query/_helpers/immutable-query-key-helper.ts create mode 100644 apps/web/src/query/composables/use-api-query-key.ts diff --git a/apps/web/src/query/_helpers/immutable-query-key-helper.ts b/apps/web/src/query/_helpers/immutable-query-key-helper.ts new file mode 100644 index 0000000000..9de930582d --- /dev/null +++ b/apps/web/src/query/_helpers/immutable-query-key-helper.ts @@ -0,0 +1,16 @@ +export const createImmutableObjectKeyItem = >(obj: T): T => { + if (obj === null || typeof obj !== 'object') { + return obj; + } + + if (Array.isArray(obj)) { + return obj.map((item) => createImmutableObjectKeyItem(item)) as unknown as T; + } + + const immutableObj = Object.entries(obj).reduce((acc, [key, value]) => ({ + ...acc, + [key]: createImmutableObjectKeyItem(value), + }), {}); + + return Object.freeze(immutableObj) as T; +}; diff --git a/apps/web/src/query/composables/use-api-query-key.ts b/apps/web/src/query/composables/use-api-query-key.ts new file mode 100644 index 0000000000..d00998b66a --- /dev/null +++ b/apps/web/src/query/composables/use-api-query-key.ts @@ -0,0 +1,85 @@ +import { toValue } from '@vueuse/core'; +import type { ComputedRef, Ref } from 'vue'; +import { computed } from 'vue'; + +import { createImmutableObjectKeyItem } from '@/query/_helpers/immutable-query-key-helper'; +import type { + QueryKeyArray, ResourceName, ServiceName, Verb, +} from '@/query/_types/query-key-type'; +import { useQueryKeyAppContext } from '@/query/composables/use-app-context-query-key'; + + + +type _MaybeRefOrGetter = T | Ref | (() => T); + +/** + * Options for generating API query keys. + * + * While the options are provided as an object where the order of keys doesn't matter, + * the generated query key will always follow this structure: + * + * ```typescript + * [ + * ...globalContext, + * service, + * resource, + * verb, + * id?, // Optional, appears first in namespace if present + * params, // Required, always present + * deps? // Optional, appears last if present + * ] + * + * Note: The order of keys in the options object doesn't affect the final query key structure. + * The query key will always maintain the above order, ensuring predictable cache management. + * + * @property id - Optional identifier for single resource operations (e.g., get, load). + * When present, it appears first in the namespace part of the query key, + * enabling hierarchical cache management for single-resource operations. + * @property params - Required parameters for the API request. + * @property deps - Optional dependencies that affect the query key. + */ +interface UseAPIQueryKeyOptions

{ + id?: _MaybeRefOrGetter; + params: _MaybeRefOrGetter

; + deps?: _MaybeRefOrGetter; +} + +interface UseAPIQueryKeyResult

{ + key: ComputedRef; + params: ComputedRef

; + deps?: ComputedRef; + id?: ComputedRef; +} + +export const _useAPIQueryKey = , V extends Verb, P extends object>( + service: S, + resource: R, + verb: V, + options: UseAPIQueryKeyOptions

, +): UseAPIQueryKeyResult

=> { + 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)), + deps: deps ? computed(() => toValue(deps)) : undefined, + id: id ? computed(() => toValue(id)) : undefined, + }; +}; From 27134bf412667073a4facfdf4e63d037d2a279b2 Mon Sep 17 00:00:00 2001 From: Yongtae Park Date: Tue, 25 Mar 2025 16:39:21 +0900 Subject: [PATCH 05/17] fix(service-context-query): apply new query key Signed-off-by: samuel.park --- .gitignore | 1 + .../_composables/use-widget-form-query.ts | 101 ++++++------- .../modules/widgets/_widgets/table/Table.vue | 142 ++++++++++-------- .../composables/use-dashboard-detail-query.ts | 77 +++++----- .../composables/use-dashboard-query.ts | 109 ++++++-------- 5 files changed, 206 insertions(+), 224 deletions(-) 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/common/modules/widgets/_composables/use-widget-form-query.ts b/apps/web/src/common/modules/widgets/_composables/use-widget-form-query.ts index 4c5bac911e..4d6563920a 100644 --- a/apps/web/src/common/modules/widgets/_composables/use-widget-form-query.ts +++ b/apps/web/src/common/modules/widgets/_composables/use-widget-form-query.ts @@ -11,6 +11,7 @@ import { usePrivateWidgetApi } from '@/api-clients/dashboard/private-widget/comp import { usePublicDataTableApi } from '@/api-clients/dashboard/public-data-table/composables/use-public-data-table-api'; import type { DataTableUpdateParameters } from '@/api-clients/dashboard/public-data-table/schema/api-verbs/update'; import { usePublicWidgetApi } from '@/api-clients/dashboard/public-widget/composables/use-public-widget-api'; +import { _useAPIQueryKey } from '@/query/composables/use-api-query-key'; import type { DataTableModel } from '@/common/modules/widgets/types/widget-data-table-type'; @@ -31,14 +32,8 @@ interface UseWidgetFormQueryReturn { keys: { publicWidgetGetQueryKey: ComputedRef; privateWidgetGetQueryKey: ComputedRef; - publicWidgetLoadQueryKey: ComputedRef; - privateWidgetLoadQueryKey: ComputedRef; - publicWidgetLoadSumQueryKey: ComputedRef; - privateWidgetLoadSumQueryKey: ComputedRef; publicDataTableListQueryKey: ComputedRef; privateDataTableListQueryKey: ComputedRef; - publicDataTableLoadQueryKey: ComputedRef; - privateDataTableLoadQueryKey: ComputedRef; }; api: { publicWidgetAPI: ReturnType['publicWidgetAPI']; @@ -59,70 +54,68 @@ export const useWidgetFormQuery = ({ }: UseWidgetFormQueryOptions): UseWidgetFormQueryReturn => { const { publicWidgetAPI, - publicWidgetGetQueryKey, - publicWidgetLoadQueryKey, - publicWidgetLoadSumQueryKey, } = usePublicWidgetApi(); const { privateWidgetAPI, - privateWidgetGetQueryKey, - privateWidgetLoadQueryKey, - privateWidgetLoadSumQueryKey, } = usePrivateWidgetApi(); const { publicDataTableAPI, - publicDataTableListQueryKey, - publicDataTableLoadQueryKey, } = usePublicDataTableApi(); const { privateDataTableAPI, - privateDataTableListQueryKey, - privateDataTableLoadQueryKey, } = usePrivateDataTableApi(); const queryClient = useQueryClient(); const isPrivate = computed(() => !!widgetId?.value?.startsWith('private')); /* Query Keys */ - const _publicWidgetGetQueryKey = computed(() => [ - ...publicWidgetGetQueryKey.value, - widgetId?.value, - ]); - const _privateWidgetGetQueryKey = computed(() => [ - ...privateWidgetGetQueryKey.value, - widgetId?.value, - ]); - const _publicDataTableListQueryKey = computed(() => [ - ...publicDataTableListQueryKey.value, - widgetId?.value, - ]); - const _privateDataTableListQueryKey = computed(() => [ - ...privateDataTableListQueryKey.value, - widgetId?.value, - ]); + const { key: publicWidgetGetQueryKey, params: publicWidgetGetParams } = _useAPIQueryKey('dashboard', 'public-widget', 'get', { + params: computed(() => ({ + widget_id: widgetId?.value as string, + })), + }); + const { + key: privateWidgetGetQueryKey, + params: privateWidgetGetParams, + } = _useAPIQueryKey('dashboard', 'private-widget', 'get', { + params: computed(() => ({ + widget_id: widgetId?.value as string, + })), + }); + const { + key: publicDataTableListQueryKey, + params: publicDataTableListParams, + } = _useAPIQueryKey('dashboard', 'public-data-table', 'list', { + params: computed(() => ({ + widget_id: widgetId?.value as string, + })), + }); + const { + key: privateDataTableListQueryKey, + params: privateDataTableListParams, + } = _useAPIQueryKey('dashboard', 'private-data-table', 'list', { + params: computed(() => ({ + widget_id: widgetId?.value as string, + })), + }); + /* Querys */ const publicWidgetQuery = useScopedQuery({ - queryKey: _publicWidgetGetQueryKey, - queryFn: () => publicWidgetAPI.get({ - widget_id: widgetId?.value as string, - }), + queryKey: publicWidgetGetQueryKey, + queryFn: () => publicWidgetAPI.get(publicWidgetGetParams.value), enabled: computed(() => !!widgetId?.value && !isPrivate.value && !preventLoad), staleTime: STALE_TIME, }, ['DOMAIN', 'WORKSPACE']); const privateWidgetQuery = useScopedQuery({ - queryKey: _privateWidgetGetQueryKey, - queryFn: () => privateWidgetAPI.get({ - widget_id: widgetId?.value as string, - }), + queryKey: privateWidgetGetQueryKey, + queryFn: () => privateWidgetAPI.get(privateWidgetGetParams.value), enabled: computed(() => !!widgetId?.value && isPrivate.value && !preventLoad), staleTime: STALE_TIME, }, ['WORKSPACE']); const publicDataTableListQuery = useScopedQuery({ - queryKey: _publicDataTableListQueryKey, - queryFn: () => publicDataTableAPI.list({ - widget_id: widgetId?.value as string, - }), + queryKey: publicDataTableListQueryKey, + queryFn: () => publicDataTableAPI.list(publicDataTableListParams.value), select: (data) => data?.results || [], enabled: computed(() => !!widgetId?.value && !isPrivate.value && !preventLoad), initialData: DEFAULT_LIST_DATA, @@ -130,10 +123,8 @@ export const useWidgetFormQuery = ({ staleTime: STALE_TIME, }, ['DOMAIN', 'WORKSPACE']); const privateDataTableListQuery = useScopedQuery({ - queryKey: _privateDataTableListQueryKey, - queryFn: () => privateDataTableAPI.list({ - widget_id: widgetId?.value as string, - }), + queryKey: privateDataTableListQueryKey, + queryFn: () => privateDataTableAPI.list(privateDataTableListParams.value), select: (data) => data?.results || [], enabled: computed(() => !!widgetId?.value && isPrivate.value && !preventLoad), initialData: DEFAULT_LIST_DATA, @@ -175,16 +166,10 @@ export const useWidgetFormQuery = ({ privateDataTableAPI, }, keys: { - publicWidgetGetQueryKey: _publicWidgetGetQueryKey, - privateWidgetGetQueryKey: _privateWidgetGetQueryKey, - publicDataTableListQueryKey: _publicDataTableListQueryKey, - privateDataTableListQueryKey: _privateDataTableListQueryKey, - publicWidgetLoadQueryKey: computed(() => publicWidgetLoadQueryKey.value), - privateWidgetLoadQueryKey: computed(() => privateWidgetLoadQueryKey.value), - publicWidgetLoadSumQueryKey: computed(() => publicWidgetLoadSumQueryKey.value), - privateWidgetLoadSumQueryKey: computed(() => privateWidgetLoadSumQueryKey.value), - publicDataTableLoadQueryKey: computed(() => publicDataTableLoadQueryKey.value), - privateDataTableLoadQueryKey: computed(() => privateDataTableLoadQueryKey.value), + publicWidgetGetQueryKey, + privateWidgetGetQueryKey, + publicDataTableListQueryKey, + privateDataTableListQueryKey, }, fetcher: { updateDataTableFn, diff --git a/apps/web/src/common/modules/widgets/_widgets/table/Table.vue b/apps/web/src/common/modules/widgets/_widgets/table/Table.vue index 80cd62a945..5a4cb2e37d 100644 --- a/apps/web/src/common/modules/widgets/_widgets/table/Table.vue +++ b/apps/web/src/common/modules/widgets/_widgets/table/Table.vue @@ -10,6 +10,7 @@ import type { Sort } from '@cloudforet/core-lib/space-connector/type'; import { PPagination } from '@cloudforet/mirinae'; import type { WidgetLoadParams, WidgetLoadResponse, WidgetLoadSumParams } from '@/api-clients/dashboard/_types/widget-type'; +import { _useAPIQueryKey } from '@/query/composables/use-api-query-key'; import { i18n } from '@/translations'; import ErrorHandler from '@/common/composables/error/errorHandler'; @@ -20,9 +21,6 @@ import { useWidgetFrame } from '@/common/modules/widgets/_composables/use-widget import { DATA_TABLE_OPERATOR } from '@/common/modules/widgets/_constants/data-table-constant'; import { WIDGET_LOAD_STALE_TIME } from '@/common/modules/widgets/_constants/widget-constant'; import { SUB_TOTAL_NAME } from '@/common/modules/widgets/_constants/widget-field-constant'; -import { - normalizeAndSerializeVars, -} from '@/common/modules/widgets/_helpers/global-variable-helper'; import type { CustomTableColumnWidthValue } from '@/common/modules/widgets/_widget-fields/custom-table-column-width/type'; import type { DataFieldHeatmapColorValue } from '@/common/modules/widgets/_widget-fields/data-field-heatmap-color/type'; import type { DataFieldValue } from '@/common/modules/widgets/_widget-fields/data-field/type'; @@ -53,10 +51,83 @@ const REFERENCE_FIELDS = ['Project', 'Workspace', 'Region', 'Service Account']; const props = defineProps(); const emit = defineEmits(); -const { keys, api } = useWidgetFormQuery({ +const { api } = useWidgetFormQuery({ widgetId: computed(() => props.widgetId), preventLoad: true, }); +const { key: publicWidgetLoadQueryKey, params: publicWidgetLoadParams } = _useAPIQueryKey('dashboard', 'public-widget', 'load', { + id: computed(() => props.widgetId), + params: computed(() => ({ + widget_id: props.widgetId, + start: dateRange.value.start, + end: dateRange.value.end, + sort: getTableDefaultSortBy(state.sortBy), + page: { + start: (state.pageSize * (state.thisPage - 1)) + 1, + limit: state.pageSize, + }, + group_by: (widgetOptionsState.groupByInfo?.data as string[]) ?? [], + vars: props.dashboardVars, + granularity: widgetOptionsState.granularityInfo?.granularity, + })), + deps: computed(() => ({ + widgetName: props.widgetName, + dataTableId: props.dataTableId, + })), +}); + +const { key: privateWidgetLoadQueryKey, params: privateWidgetLoadParams } = _useAPIQueryKey('dashboard', 'private-widget', 'load', { + id: computed(() => props.widgetId), + params: computed(() => ({ + widget_id: props.widgetId, + start: dateRange.value.start, + end: dateRange.value.end, + sort: getTableDefaultSortBy(state.sortBy), + page: { + start: (state.pageSize * (state.thisPage - 1)) + 1, + limit: state.pageSize, + }, + group_by: (widgetOptionsState.groupByInfo?.data as string[]) ?? [], + vars: props.dashboardVars, + granularity: widgetOptionsState.granularityInfo?.granularity, + })), + deps: computed(() => ({ + widgetName: props.widgetName, + dataTableId: props.dataTableId, + })), +}); + +const { key: publicWidgetLoadSumQueryKey, params: publicWidgetLoadSumParams } = _useAPIQueryKey('dashboard', 'public-widget', 'load-sum', { + id: computed(() => props.widgetId), + params: computed(() => ({ + widget_id: props.widgetId, + start: dateRange.value.start, + end: dateRange.value.end, + vars: props.dashboardVars, + granularity: widgetOptionsState.granularityInfo?.granularity, + })), + deps: computed(() => ({ + widgetName: props.widgetName, + dataTableId: props.dataTableId, + enabledTotal: !!widgetOptionsState.totalInfo?.toggleValue, + })), +}); + +const { key: privateWidgetLoadSumQueryKey, params: privateWidgetLoadSumParams } = _useAPIQueryKey('dashboard', 'private-widget', 'load-sum', { + id: computed(() => props.widgetId), + params: computed(() => ({ + widget_id: props.widgetId, + start: dateRange.value.start, + end: dateRange.value.end, + vars: props.dashboardVars, + granularity: widgetOptionsState.granularityInfo?.granularity, + })), + deps: computed(() => ({ + widgetName: props.widgetName, + dataTableId: props.dataTableId, + enabledTotal: !!widgetOptionsState.totalInfo?.toggleValue, + })), +}); const { dateRange } = useWidgetDateRange({ dateRangeFieldValue: computed(() => (props.widgetOptions?.dateRange?.value as DateRangeValue)), @@ -158,60 +229,11 @@ const fetchWidgetSumData = async (params: WidgetLoadSumParams): Promise [ - ...(state.isPrivateWidget ? keys.privateWidgetLoadQueryKey.value : keys.publicWidgetLoadQueryKey.value), - props.dashboardId, - props.widgetId, - props.widgetName, - { - start: dateRange.value.start, - end: dateRange.value.end, - sort: state.sortBy, - page: state.thisPage, - pageSize: state.pageSize, - granularity: widgetOptionsState.granularityInfo?.granularity, - groupBy: widgetOptionsState.groupByInfo?.data, - dataTableId: props.dataTableId, - // dataTableOptions: normalizeAndSerializeDataTableOptions(state.dataTable?.options || {}), - // dataTables: normalizeAndSerializeDataTableOptions((props.dataTables || []).map((d) => d?.options || {})), - vars: normalizeAndSerializeVars(props.dashboardVars), - }, -]); - -const fullDataQueryKey = computed(() => [ - ...(state.isPrivateWidget ? keys.privateWidgetLoadSumQueryKey.value : keys.publicWidgetLoadSumQueryKey.value), - props.dashboardId, - props.widgetId, - props.widgetName, - { - start: dateRange.value.start, - end: dateRange.value.end, - granularity: widgetOptionsState.granularityInfo?.granularity, - dataTableId: props.dataTableId, - // dataTableOptions: normalizeAndSerializeDataTableOptions(state.dataTable?.options || {}), - // dataTables: normalizeAndSerializeDataTableOptions((props.dataTables || []).map((d) => d?.options || {})), - enabledTotal: !!widgetOptionsState.totalInfo?.toggleValue, - vars: normalizeAndSerializeVars(props.dashboardVars), - }, -]); - const queryResults = useQueries({ queries: [ { - queryKey: baseQueryKey, - queryFn: () => fetchWidgetData({ - widget_id: props.widgetId, - start: dateRange.value.start, - end: dateRange.value.end, - sort: getTableDefaultSortBy(state.sortBy), - page: { - start: (state.pageSize * (state.thisPage - 1)) + 1, - limit: state.pageSize, - }, - group_by: (widgetOptionsState.groupByInfo?.data as string[]) ?? [], - vars: props.dashboardVars, - granularity: widgetOptionsState.granularityInfo?.granularity, - }), + queryKey: state.isPrivateWidget ? privateWidgetLoadQueryKey : publicWidgetLoadQueryKey, + queryFn: () => fetchWidgetData(state.isPrivateWidget ? privateWidgetLoadParams.value : publicWidgetLoadParams.value), enabled: computed(() => { const widgetActive = props.widgetState !== 'INACTIVE'; const dataTableReady = !!state.dataTable; @@ -221,14 +243,8 @@ const queryResults = useQueries({ staleTime: WIDGET_LOAD_STALE_TIME, }, { - queryKey: fullDataQueryKey, - queryFn: () => fetchWidgetSumData({ - widget_id: props.widgetId, - start: dateRange.value.start, - end: dateRange.value.end, - vars: props.dashboardVars, - granularity: widgetOptionsState.granularityInfo?.granularity, - }), + queryKey: state.isPrivateWidget ? privateWidgetLoadSumQueryKey : publicWidgetLoadSumQueryKey, + queryFn: () => fetchWidgetSumData(state.isPrivateWidget ? privateWidgetLoadSumParams.value : publicWidgetLoadSumParams.value), enabled: computed(() => { const widgetActive = props.widgetState !== 'INACTIVE'; const dataTableReady = !!state.dataTable; diff --git a/apps/web/src/services/dashboards/composables/use-dashboard-detail-query.ts b/apps/web/src/services/dashboards/composables/use-dashboard-detail-query.ts index 9049e25f79..b7f7210fb7 100644 --- a/apps/web/src/services/dashboards/composables/use-dashboard-detail-query.ts +++ b/apps/web/src/services/dashboards/composables/use-dashboard-detail-query.ts @@ -8,23 +8,28 @@ import { useScopedQuery } from '@/api-clients/_common/composables/use-scoped-que import type { DashboardModel, DashboardUpdateParams } from '@/api-clients/dashboard/_types/dashboard-type'; import type { WidgetModel, WidgetUpdateParams } from '@/api-clients/dashboard/_types/widget-type'; import { usePrivateDashboardApi } from '@/api-clients/dashboard/private-dashboard/composables/use-private-dashboard-api'; +import type { PrivateDashboardGetParameters } from '@/api-clients/dashboard/private-dashboard/schema/api-verbs/get'; import type { PrivateDashboardUpdateParameters } from '@/api-clients/dashboard/private-dashboard/schema/api-verbs/update'; import type { PrivateDashboardModel } from '@/api-clients/dashboard/private-dashboard/schema/model'; import { usePrivateWidgetApi } from '@/api-clients/dashboard/private-widget/composables/use-private-widget-api'; +import type { PrivateWidgetListParameters } from '@/api-clients/dashboard/private-widget/schema/api-verbs/list'; import type { PrivateWidgetUpdateParameters } from '@/api-clients/dashboard/private-widget/schema/api-verbs/update'; import type { PrivateWidgetModel } from '@/api-clients/dashboard/private-widget/schema/model'; import { usePublicDashboardApi } from '@/api-clients/dashboard/public-dashboard/composables/use-public-dashboard-api'; +import type { PublicDashboardGetParameters } from '@/api-clients/dashboard/public-dashboard/schema/api-verbs/get'; 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 { usePublicWidgetApi } from '@/api-clients/dashboard/public-widget/composables/use-public-widget-api'; +import type { PublicWidgetListParameters } from '@/api-clients/dashboard/public-widget/schema/api-verbs/list'; import type { PublicWidgetUpdateParameters } from '@/api-clients/dashboard/public-widget/schema/api-verbs/update'; import type { PublicWidgetModel } from '@/api-clients/dashboard/public-widget/schema/model'; +import { _useAPIQueryKey } from '@/query/composables/use-api-query-key'; const DEFAULT_LIST_DATA = { results: [] }; const STALE_TIME = 1000 * 60 * 5; interface UseDashboardDetailQueryOptions { - dashboardId: ComputedRef; + dashboardId: ComputedRef; } interface UseDashboardDetailQueryReturn { @@ -54,54 +59,46 @@ interface UseDashboardDetailQueryReturn { export const useDashboardDetailQuery = ({ dashboardId, }: UseDashboardDetailQueryOptions): UseDashboardDetailQueryReturn => { - const { publicDashboardAPI, publicDashboardGetQueryKey } = usePublicDashboardApi(); - const { privateDashboardAPI, privateDashboardGetQueryKey } = usePrivateDashboardApi(); - const { publicWidgetAPI, publicWidgetListQueryKey } = usePublicWidgetApi(); - const { privateWidgetAPI, privateWidgetListQueryKey } = usePrivateWidgetApi(); + const { publicDashboardAPI } = usePublicDashboardApi(); + const { privateDashboardAPI } = usePrivateDashboardApi(); + const { publicWidgetAPI } = usePublicWidgetApi(); + const { privateWidgetAPI } = usePrivateWidgetApi(); const queryClient = useQueryClient(); const isPrivate = computed(() => !!dashboardId.value?.startsWith('private')); /* Query Keys */ - const _publicDashboardGetQueryKey = computed(() => [ - ...publicDashboardGetQueryKey.value, - dashboardId.value, - ]); - const _privateDashboardGetQueryKey = computed(() => [ - ...privateDashboardGetQueryKey.value, - dashboardId.value, - ]); - const _publicWidgetListQueryKey = computed(() => [ - ...publicWidgetListQueryKey.value, - dashboardId.value, - ]); - const _privateWidgetListQueryKey = computed(() => [ - ...privateWidgetListQueryKey.value, - dashboardId.value, - ]); + const { key: publicDashboardGetQueryKey, params: publicDashboardGetParams } = _useAPIQueryKey('dashboard', 'public-dashboard', 'get', { + id: dashboardId, + params: computed(() => ({ dashboard_id: dashboardId.value })), + }); + const { key: privateDashboardGetQueryKey, params: privateDashboardGetParams } = _useAPIQueryKey('dashboard', 'private-dashboard', 'get', { + id: dashboardId, + params: computed(() => ({ dashboard_id: dashboardId.value })), + }); + const { key: publicWidgetListQueryKey, params: publicWidgetListParams } = _useAPIQueryKey('dashboard', 'public-widget', 'list', { + params: computed(() => ({ dashboard_id: dashboardId.value })), + }); + const { key: privateWidgetListQueryKey, params: privateWidgetListParams } = _useAPIQueryKey('dashboard', 'private-widget', 'list', { + params: computed(() => ({ dashboard_id: dashboardId.value })), + }); /* Querys */ const publicDashboardQuery = useScopedQuery({ - queryKey: _publicDashboardGetQueryKey, - queryFn: () => publicDashboardAPI.get({ - dashboard_id: dashboardId.value as string, - }), + queryKey: publicDashboardGetQueryKey, + queryFn: () => publicDashboardAPI.get(publicDashboardGetParams.value), enabled: computed(() => !!dashboardId.value && !isPrivate.value), staleTime: STALE_TIME, }, ['DOMAIN', 'WORKSPACE']); const privateDashboardQuery = useScopedQuery({ - queryKey: _privateDashboardGetQueryKey, - queryFn: () => privateDashboardAPI.get({ - dashboard_id: dashboardId.value as string, - }), + queryKey: privateDashboardGetQueryKey, + queryFn: () => privateDashboardAPI.get(privateDashboardGetParams.value), enabled: computed(() => !!dashboardId.value && isPrivate.value), staleTime: STALE_TIME, }, ['WORKSPACE']); const publicWidgetListQuery = useScopedQuery({ - queryKey: _publicWidgetListQueryKey, - queryFn: () => publicWidgetAPI.list({ - dashboard_id: dashboardId.value as string, - }), + queryKey: publicWidgetListQueryKey, + queryFn: () => publicWidgetAPI.list(publicWidgetListParams.value), select: (data) => data?.results || [], enabled: computed(() => !!dashboardId.value && publicDashboardQuery.isSuccess && !isPrivate.value), initialData: DEFAULT_LIST_DATA, @@ -109,10 +106,8 @@ export const useDashboardDetailQuery = ({ staleTime: STALE_TIME, }, ['DOMAIN', 'WORKSPACE']); const privateWidgetListQuery = useScopedQuery({ - queryKey: _privateWidgetListQueryKey, - queryFn: () => privateWidgetAPI.list({ - dashboard_id: dashboardId.value as string, - }), + queryKey: privateWidgetListQueryKey, + queryFn: () => privateWidgetAPI.list(privateWidgetListParams.value), select: (data) => data?.results || [], enabled: computed(() => !!dashboardId.value && privateDashboardQuery.isSuccess && isPrivate.value), initialData: DEFAULT_LIST_DATA, @@ -156,10 +151,10 @@ export const useDashboardDetailQuery = ({ privateWidgetAPI, }, keys: { - publicDashboardGetQueryKey: _publicDashboardGetQueryKey, - privateDashboardGetQueryKey: _privateDashboardGetQueryKey, - publicWidgetListQueryKey: _publicWidgetListQueryKey, - privateWidgetListQueryKey: _privateWidgetListQueryKey, + publicDashboardGetQueryKey, + privateDashboardGetQueryKey, + publicWidgetListQueryKey, + privateWidgetListQueryKey, }, fetcher: { updateDashboardFn, diff --git a/apps/web/src/services/dashboards/composables/use-dashboard-query.ts b/apps/web/src/services/dashboards/composables/use-dashboard-query.ts index cd1bf71229..4def300a75 100644 --- a/apps/web/src/services/dashboards/composables/use-dashboard-query.ts +++ b/apps/web/src/services/dashboards/composables/use-dashboard-query.ts @@ -3,7 +3,6 @@ import { computed, type ComputedRef, reactive, watch, } from 'vue'; -import type { QueryKey } from '@tanstack/vue-query'; import { type QueryClient, useQueryClient } from '@tanstack/vue-query'; import { ApiQueryHelper } from '@cloudforet/core-lib/space-connector/helper'; @@ -21,6 +20,8 @@ import type { PublicDashboardModel } from '@/api-clients/dashboard/public-dashbo import { usePublicFolderApi } from '@/api-clients/dashboard/public-folder/composables/use-public-folder-api'; import type { PublicFolderUpdateParameters } from '@/api-clients/dashboard/public-folder/schema/api-verbs/update'; import type { PublicFolderModel } from '@/api-clients/dashboard/public-folder/schema/model'; +import type { QueryKeyArray } from '@/query/_types/query-key-type'; +import { _useAPIQueryKey } from '@/query/composables/use-api-query-key'; import { useAppContextStore } from '@/store/app-context/app-context-store'; import { useUserWorkspaceStore } from '@/store/app-context/workspace/user-workspace-store'; @@ -38,10 +39,10 @@ interface UseDashboardQueryReturn { privateFolderList: Ref; isLoading: ComputedRef; keys: { - publicDashboardListQueryKey: ComputedRef; - privateDashboardListQueryKey: ComputedRef; - publicFolderListQueryKey: ComputedRef; - privateFolderListQueryKey: ComputedRef; + publicDashboardListQueryKey: ComputedRef; + privateDashboardListQueryKey: ComputedRef; + publicFolderListQueryKey: ComputedRef; + privateFolderListQueryKey: ComputedRef; }; fetcher: { updateFolderFn: (args: FolderUpdateParams) => Promise @@ -56,10 +57,11 @@ interface UseDashboardQueryReturn { } export const useDashboardQuery = (): UseDashboardQueryReturn => { - const { publicDashboardAPI, publicDashboardListQueryKey } = usePublicDashboardApi(); - const { privateDashboardAPI, privateDashboardListQueryKey } = usePrivateDashboardApi(); - const { publicFolderAPI, publicFolderListQueryKey } = usePublicFolderApi(); - const { privateFolderAPI, privateFolderListQueryKey } = usePrivateFolderApi(); + const { publicDashboardAPI } = usePublicDashboardApi(); + const { privateDashboardAPI } = usePrivateDashboardApi(); + const { publicFolderAPI } = usePublicFolderApi(); + const { privateFolderAPI } = usePrivateFolderApi(); + const queryClient = useQueryClient(); @@ -86,52 +88,44 @@ export const useDashboardQuery = (): UseDashboardQueryReturn => { }, }); - /* Query Keys */ - const _publicDashboardListQueryKey = computed(() => [ - ...publicDashboardListQueryKey.value, - _state.defaultListQuery, - _state.publicDashboardListApiQuery.filter, - ]); - const _privateDashboardListQueryKey = computed(() => [ - ...privateDashboardListQueryKey.value, - _state.defaultListQuery, - ]); - const _publicFolderListQueryKey = computed(() => [ - ...publicFolderListQueryKey.value, - _state.defaultListQuery, - _state.publicFolderListApiQuery.filter, - ]); - const _privateFolderListQueryKey = computed(() => [ - ...privateFolderListQueryKey.value, - _state.defaultListQuery, - ]); - - - /* Querys */ - const publicDashboardListQuery = useScopedQuery, unknown, PublicDashboardModel[]>({ - queryKey: _publicDashboardListQueryKey, - queryFn: () => publicDashboardAPI.list({ + /* Query Keys and Params */ + const { key: publicDashboardListQueryKey, params: publicDashboardListParams } = _useAPIQueryKey('dashboard', 'public-dashboard', 'list', { + params: computed(() => ({ query: { ..._state.publicDashboardListApiQuery, - sort: [{ - key: 'created_at', - desc: true, - }], + ..._state.defaultListQuery.query, }, - }), + })), + }); + const { key: privateDashboardListQueryKey, params: privateDashboardListParams } = _useAPIQueryKey('dashboard', 'private-dashboard', 'list', { + params: computed(() => _state.defaultListQuery), + }); + const { key: publicFolderListQueryKey, params: publicFolderListParams } = _useAPIQueryKey('dashboard', 'public-folder', 'list', { + params: computed(() => ({ + query: { + ..._state.publicFolderListApiQuery, + ..._state.defaultListQuery.query, + }, + })), + }); + const { key: privateFolderListQueryKey, params: privateFolderListParams } = _useAPIQueryKey('dashboard', 'private-folder', 'list', { + params: computed(() => _state.defaultListQuery), + }); + + /* Querys */ + const publicDashboardListQuery = useScopedQuery, unknown, PublicDashboardModel[]>({ + queryKey: publicDashboardListQueryKey, + queryFn: () => publicDashboardAPI.list(publicDashboardListParams.value), select: (data) => data?.results ?? [], initialData: DEFAULT_LIST_DATA, initialDataUpdatedAt: 0, staleTime: STALE_TIME, enabled: computed(() => !!_state.publicDashboardListApiQuery?.filter), }, ['DOMAIN', 'WORKSPACE']); + const privateDashboardListQuery = useScopedQuery, unknown, PrivateDashboardModel[]>({ - queryKey: _privateDashboardListQueryKey, - queryFn: () => privateDashboardAPI.list({ - query: { - sort: [{ key: 'created_at', desc: true }], - }, - }), + queryKey: privateDashboardListQueryKey, + queryFn: () => privateDashboardAPI.list(privateDashboardListParams.value), select: (data) => data?.results || [], initialData: DEFAULT_LIST_DATA, initialDataUpdatedAt: 0, @@ -139,13 +133,8 @@ export const useDashboardQuery = (): UseDashboardQueryReturn => { enabled: computed(() => !_state.isAdminMode), }, ['WORKSPACE']); const publicFolderListQuery = useScopedQuery, unknown, PublicFolderModel[]>({ - queryKey: _publicFolderListQueryKey, - queryFn: () => publicFolderAPI.list({ - query: { - ..._state.publicFolderListApiQuery, - sort: [{ key: 'created_at', desc: true }], - }, - }), + queryKey: publicFolderListQueryKey, + queryFn: () => publicFolderAPI.list(publicFolderListParams.value), select: (data) => data?.results || [], initialData: DEFAULT_LIST_DATA, initialDataUpdatedAt: 0, @@ -153,12 +142,8 @@ export const useDashboardQuery = (): UseDashboardQueryReturn => { enabled: computed(() => !!_state.publicFolderListApiQuery?.filter), }, ['DOMAIN', 'WORKSPACE']); const privateFolderListQuery = useScopedQuery, unknown, PrivateFolderModel[]>({ - queryKey: _privateFolderListQueryKey, - queryFn: () => privateFolderAPI.list({ - query: { - sort: [{ key: 'created_at', desc: true }], - }, - }), + queryKey: privateFolderListQueryKey, + queryFn: () => privateFolderAPI.list(privateFolderListParams.value), select: (data) => data?.results || [], initialData: DEFAULT_LIST_DATA, initialDataUpdatedAt: 0, @@ -199,10 +184,10 @@ export const useDashboardQuery = (): UseDashboardQueryReturn => { privateFolderList: computed(() => privateFolderListQuery.data.value ?? []), isLoading, keys: { - publicDashboardListQueryKey: _publicDashboardListQueryKey, - privateDashboardListQueryKey: _privateDashboardListQueryKey, - publicFolderListQueryKey: _publicFolderListQueryKey, - privateFolderListQueryKey: _privateFolderListQueryKey, + publicDashboardListQueryKey, + privateDashboardListQueryKey, + publicFolderListQueryKey, + privateFolderListQueryKey, }, fetcher: { updateFolderFn, From 2d8c6f796aea0cc952c45f2da7fa6b926737085d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=9A=A9=ED=83=9C?= Date: Wed, 26 Mar 2025 17:00:27 +0900 Subject: [PATCH 06/17] chore(query-key): edit dir architecture & add doc/test/debugger Signed-off-by: piggggggggy --- .../__tests__/use-api-query-key.test.ts | 198 ++++++++++++++++++ .../_composable}/use-app-context-query-key.ts | 0 .../_helpers/immutable-query-key-helper.ts | 0 .../{ => query-key}/_types/query-key-type.ts | 0 .../src/query/query-key/use-api-query-key.md | 145 +++++++++++++ .../use-api-query-key.ts | 49 ++++- 6 files changed, 389 insertions(+), 3 deletions(-) create mode 100644 apps/web/src/query/query-key/__tests__/use-api-query-key.test.ts rename apps/web/src/query/{composables => query-key/_composable}/use-app-context-query-key.ts (100%) rename apps/web/src/query/{ => query-key}/_helpers/immutable-query-key-helper.ts (100%) rename apps/web/src/query/{ => query-key}/_types/query-key-type.ts (100%) create mode 100644 apps/web/src/query/query-key/use-api-query-key.md rename apps/web/src/query/{composables => query-key}/use-api-query-key.ts (63%) diff --git a/apps/web/src/query/query-key/__tests__/use-api-query-key.test.ts b/apps/web/src/query/query-key/__tests__/use-api-query-key.test.ts new file mode 100644 index 0000000000..8fa4ee4318 --- /dev/null +++ b/apps/web/src/query/query-key/__tests__/use-api-query-key.test.ts @@ -0,0 +1,198 @@ +import { ref } from 'vue'; + +import { + describe, it, expect, vi, +} from 'vitest'; + +import { _useAPIQueryKey } from '../use-api-query-key'; + +// Mock useQueryKeyAppContext +vi.mock('@/query/query-key/_composable/use-app-context-query-key', () => ({ + useQueryKeyAppContext: () => ({ + value: ['workspace', 'workspace-123'] as const, + }), +})); + +describe('_useAPIQueryKey', () => { + it('should generate correct query key structure for basic usage', () => { + const { key } = _useAPIQueryKey( + 'dashboard', + 'public-data-table', + 'list', + { + params: { page: 1, limit: 10 }, + }, + ); + + expect(key.value).toMatchInlineSnapshot(` +[ + "workspace", + "workspace-123", + "dashboard", + "public-data-table", + "list", + { + "limit": 10, + "page": 1, + }, +] +`); + }); + + it('should generate correct query key structure with id', () => { + const { key } = _useAPIQueryKey( + 'dashboard', + 'public-data-table', + 'get', + { + id: 'table-123', + params: { id: 'table-123' }, + }, + ); + + expect(key.value).toMatchInlineSnapshot(` +[ + "workspace", + "workspace-123", + "dashboard", + "public-data-table", + "get", + "table-123", + { + "id": "table-123", + }, +] +`); + }); + + it('should generate correct query key structure with deps', () => { + const { key } = _useAPIQueryKey( + 'dashboard', + 'public-data-table', + 'list', + { + params: { page: 1, limit: 10 }, + deps: { filter: 'active' }, + }, + ); + + expect(key.value).toMatchInlineSnapshot(` +[ + "workspace", + "workspace-123", + "dashboard", + "public-data-table", + "list", + { + "limit": 10, + "page": 1, + }, + { + "filter": "active", + }, +] +`); + }); + + it('should handle reactive values correctly', () => { + const params = ref({ id: 'table-123' }); + const deps = ref({ filter: 'active' }); + const id = ref('table-123'); + + const { key } = _useAPIQueryKey( + 'dashboard', + 'public-data-table', + 'get', + { + id, + params, + deps, + }, + ); + + expect(key.value).toMatchInlineSnapshot(` +[ + "workspace", + "workspace-123", + "dashboard", + "public-data-table", + "get", + "table-123", + { + "id": "table-123", + }, + { + "filter": "active", + }, +] +`); + }); + + it('should handle function getters correctly', () => { + const { key } = _useAPIQueryKey( + 'dashboard', + 'public-data-table', + 'get', + { + id: () => 'table-123', + params: () => ({ id: 'table-123' }), + deps: () => ({ filter: 'active' }), + }, + ); + + expect(key.value).toMatchInlineSnapshot(` +[ + "workspace", + "workspace-123", + "dashboard", + "public-data-table", + "get", + "table-123", + { + "id": "table-123", + }, + { + "filter": "active", + } +] +`); + }); + + it('should maintain consistent key structure with different option orders', () => { + const params = { id: 'table-123' }; + const deps = { filter: 'active' }; + const id = 'table-123'; + + const { key: key1 } = _useAPIQueryKey( + 'dashboard', + 'public-data-table', + 'get', + { id, params, deps }, + ); + + const { key: key2 } = _useAPIQueryKey( + 'dashboard', + 'public-data-table', + 'get', + { deps, params, id }, + ); + + expect(key1.value).toEqual(key2.value); + expect(key1.value).toMatchInlineSnapshot(` +[ + "workspace", + "workspace-123", + "dashboard", + "public-data-table", + "get", + "table-123", + { + "id": "table-123", + }, + { + "filter": "active", + } +] +`); + }); +}); diff --git a/apps/web/src/query/composables/use-app-context-query-key.ts b/apps/web/src/query/query-key/_composable/use-app-context-query-key.ts similarity index 100% rename from apps/web/src/query/composables/use-app-context-query-key.ts rename to apps/web/src/query/query-key/_composable/use-app-context-query-key.ts diff --git a/apps/web/src/query/_helpers/immutable-query-key-helper.ts b/apps/web/src/query/query-key/_helpers/immutable-query-key-helper.ts similarity index 100% rename from apps/web/src/query/_helpers/immutable-query-key-helper.ts rename to apps/web/src/query/query-key/_helpers/immutable-query-key-helper.ts diff --git a/apps/web/src/query/_types/query-key-type.ts b/apps/web/src/query/query-key/_types/query-key-type.ts similarity index 100% rename from apps/web/src/query/_types/query-key-type.ts rename to apps/web/src/query/query-key/_types/query-key-type.ts diff --git a/apps/web/src/query/query-key/use-api-query-key.md b/apps/web/src/query/query-key/use-api-query-key.md new file mode 100644 index 0000000000..7eca13f70c --- /dev/null +++ b/apps/web/src/query/query-key/use-api-query-key.md @@ -0,0 +1,145 @@ +# API Query Key Composable + +## Overview +`useAPIQueryKey` is a composable for generating query keys for API requests. It integrates with Tanstack Query's caching system to provide a consistent query key structure. + +## Basic Key Structure +The query key array follows a strict order to ensure consistent caching and invalidation patterns. The order is guaranteed to be: + +1. App Context (workspace/admin context) +2. Service Name +3. Resource Name +4. Verb +5. Resource ID (if applicable) +6. Request Parameters +7. Dependencies (if applicable) + +This predictable structure allows for precise cache management and query invalidation at any level of the hierarchy. + +```typescript +[ + ...appContext, // ['workspace', 'workspaceId'] or ['admin'] + service, // service name (e.g., 'dashboard', 'opsflow') + resource, // resource name (e.g., 'public-data-table') + verb, // action (e.g., 'get', 'list', 'load') + id?, // (optional) resource identifier (with 'get' verb) + params, // request parameters + deps? // (optional) dependencies +] +``` + +## Key Options Interface + +```typescript +interface UseAPIQueryKeyOptions

{ + id?: _MaybeRefOrGetter; + params: _MaybeRefOrGetter

; + deps?: _MaybeRefOrGetter; +} +``` + +## Return Value + +```typescript +interface UseAPIQueryKeyResult

{ + key: ComputedRef; // query key + params: ComputedRef

; // parameters + deps?: ComputedRef; // dependencies (optional) + id?: ComputedRef; // ID (optional) +} +``` + + + +## Usage + +### Basic Usage +```typescript +const { key } = useAPIQueryKey( + 'dashboard', + 'public-data-table', + 'list', + { + params: computed(() => ({ page: 1, limit: 10 })), + } +); +``` + +### With ID +```typescript +const { key } = useAPIQueryKey( + 'dashboard', + 'public-data-table', + 'get', + { + id: toRef(state, 'currentDataTableId'), + params: computed(() => ({ data_table_id: 'table-123' })), + } +); +``` + +### With Dependencies +```typescript +const { key } = _useAPIQueryKey( + 'dashboard', + 'public-data-table', + 'list', + { + params: computed(() => ({ page: 1, limit: 10 }), + deps: computed(() => ({ filter: 'active' }), + } +); +``` + +### With useQuery +```typescript +const { key, params } = _useAPIQueryKey( + 'dashboard', + 'public-data-table', + 'list', + { + params: computed(() => state.params) + } +); + +const { data } = useQuery({ + queryKey: key, + queryFn: () => publicDataTableAPI.list(params.value) +}); +``` + +### Best Practices +1. Always type your params interface for better type safety +2. Use the returned `params` in your `queryFn` to maintain consistency +3. Leverage TypeScript's type inference for better development experience +4. Consider using `deps` for values that should trigger cache invalidation but aren't part of the API request + + +## Maintenance Guide + +### Type Definitions +- `ServiceName`: Available service names +- `ResourceName`: Available resource names per service +- `Verb`: Available actions for service and resource + +### Development Environment Validation +- Runtime validation only in development environment +- Validation for required parameters, types, and structure +- Debugging support through warning messages + +### Testing +- Snapshot tests for query key structure validation +- Test cases for various usage scenarios +- Tests for reactive values and function getters + +## Important Notes +1. Query keys must maintain immutability. +2. Object parameters are automatically converted to immutable objects. +3. Runtime validation only runs in development environment. +4. Option object key order does not affect the result. + +## References +- Integrates with Tanstack Query's caching system +- Supports Vue's reactivity system +- Ensures TypeScript type safety +- Provides logging for debugging in development environment diff --git a/apps/web/src/query/composables/use-api-query-key.ts b/apps/web/src/query/query-key/use-api-query-key.ts similarity index 63% rename from apps/web/src/query/composables/use-api-query-key.ts rename to apps/web/src/query/query-key/use-api-query-key.ts index d00998b66a..238b101835 100644 --- a/apps/web/src/query/composables/use-api-query-key.ts +++ b/apps/web/src/query/query-key/use-api-query-key.ts @@ -2,11 +2,11 @@ import { toValue } from '@vueuse/core'; import type { ComputedRef, Ref } from 'vue'; import { computed } from 'vue'; -import { createImmutableObjectKeyItem } from '@/query/_helpers/immutable-query-key-helper'; +import { useQueryKeyAppContext } from '@/query/query-key/_composable/use-app-context-query-key'; +import { createImmutableObjectKeyItem } from '@/query/query-key/_helpers/immutable-query-key-helper'; import type { QueryKeyArray, ResourceName, ServiceName, Verb, -} from '@/query/_types/query-key-type'; -import { useQueryKeyAppContext } from '@/query/composables/use-app-context-query-key'; +} from '@/query/query-key/_types/query-key-type'; @@ -57,6 +57,14 @@ export const _useAPIQueryKey = verb: V, options: UseAPIQueryKeyOptions

, ): UseAPIQueryKeyResult

=> { + // Runtime validation for development environment + if (import.meta.env.DEV) { + if (!service || !resource || !verb) { + console.warn('Required parameters (service, resource, verb) must be provided'); + } + _validateQueryKeyOptions(options); + } + const { id, params, deps } = options; const queryKeyAppContext = useQueryKeyAppContext(); @@ -76,6 +84,12 @@ export const _useAPIQueryKey = ]; }); + + // NOTE: Only for development environment. After using tanstack query devtools, this will be removed. + if (import.meta.env.DEV) { + console.debug(`[QueryKey] ${String(service)}/${String(resource)}/${String(verb)}`, JSON.stringify(queryKey.value, null, 2)); + } + return { key: queryKey, params: computed(() => toValue(params)), @@ -83,3 +97,32 @@ export const _useAPIQueryKey = id: id ? computed(() => toValue(id)) : undefined, }; }; + + + +const _validateQueryKeyOptions =

(options: { + id?: _MaybeRefOrGetter; + params: _MaybeRefOrGetter

; + deps?: _MaybeRefOrGetter; +}) => { + if (options.params) { + const rawParams = toValue(options.params); + if (rawParams === null || typeof rawParams !== 'object') { + console.warn('params must be a non-null object'); + } + } + + if (options.deps) { + const rawDeps = toValue(options.deps); + if (rawDeps === null || typeof rawDeps !== 'object') { + console.warn('deps must be a non-null object'); + } + } + + if (options.id) { + const id = toValue(options.id); + if (typeof id !== 'string') { + console.warn('id must be a string'); + } + } +}; From b9d6b7e1ef3f7232f51ad64d6c07ccc43220e7b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=9A=A9=ED=83=9C?= Date: Wed, 26 Mar 2025 17:04:55 +0900 Subject: [PATCH 07/17] Revert "fix(service-context-query): apply new query key" This reverts commit 27134bf412667073a4facfdf4e63d037d2a279b2. Signed-off-by: piggggggggy --- .gitignore | 1 - .../_composables/use-widget-form-query.ts | 101 +++++++------ .../modules/widgets/_widgets/table/Table.vue | 142 ++++++++---------- .../composables/use-dashboard-detail-query.ts | 77 +++++----- .../composables/use-dashboard-query.ts | 109 ++++++++------ 5 files changed, 224 insertions(+), 206 deletions(-) diff --git a/.gitignore b/.gitignore index e557c2d0c6..49eba67df9 100755 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,6 @@ node_modules.nosync/ # Dev tools .DS_Store .vscode -.cursor .idea *.swp *.bak diff --git a/apps/web/src/common/modules/widgets/_composables/use-widget-form-query.ts b/apps/web/src/common/modules/widgets/_composables/use-widget-form-query.ts index 4d6563920a..4c5bac911e 100644 --- a/apps/web/src/common/modules/widgets/_composables/use-widget-form-query.ts +++ b/apps/web/src/common/modules/widgets/_composables/use-widget-form-query.ts @@ -11,7 +11,6 @@ import { usePrivateWidgetApi } from '@/api-clients/dashboard/private-widget/comp import { usePublicDataTableApi } from '@/api-clients/dashboard/public-data-table/composables/use-public-data-table-api'; import type { DataTableUpdateParameters } from '@/api-clients/dashboard/public-data-table/schema/api-verbs/update'; import { usePublicWidgetApi } from '@/api-clients/dashboard/public-widget/composables/use-public-widget-api'; -import { _useAPIQueryKey } from '@/query/composables/use-api-query-key'; import type { DataTableModel } from '@/common/modules/widgets/types/widget-data-table-type'; @@ -32,8 +31,14 @@ interface UseWidgetFormQueryReturn { keys: { publicWidgetGetQueryKey: ComputedRef; privateWidgetGetQueryKey: ComputedRef; + publicWidgetLoadQueryKey: ComputedRef; + privateWidgetLoadQueryKey: ComputedRef; + publicWidgetLoadSumQueryKey: ComputedRef; + privateWidgetLoadSumQueryKey: ComputedRef; publicDataTableListQueryKey: ComputedRef; privateDataTableListQueryKey: ComputedRef; + publicDataTableLoadQueryKey: ComputedRef; + privateDataTableLoadQueryKey: ComputedRef; }; api: { publicWidgetAPI: ReturnType['publicWidgetAPI']; @@ -54,68 +59,70 @@ export const useWidgetFormQuery = ({ }: UseWidgetFormQueryOptions): UseWidgetFormQueryReturn => { const { publicWidgetAPI, + publicWidgetGetQueryKey, + publicWidgetLoadQueryKey, + publicWidgetLoadSumQueryKey, } = usePublicWidgetApi(); const { privateWidgetAPI, + privateWidgetGetQueryKey, + privateWidgetLoadQueryKey, + privateWidgetLoadSumQueryKey, } = usePrivateWidgetApi(); const { publicDataTableAPI, + publicDataTableListQueryKey, + publicDataTableLoadQueryKey, } = usePublicDataTableApi(); const { privateDataTableAPI, + privateDataTableListQueryKey, + privateDataTableLoadQueryKey, } = usePrivateDataTableApi(); const queryClient = useQueryClient(); const isPrivate = computed(() => !!widgetId?.value?.startsWith('private')); /* Query Keys */ - const { key: publicWidgetGetQueryKey, params: publicWidgetGetParams } = _useAPIQueryKey('dashboard', 'public-widget', 'get', { - params: computed(() => ({ - widget_id: widgetId?.value as string, - })), - }); - const { - key: privateWidgetGetQueryKey, - params: privateWidgetGetParams, - } = _useAPIQueryKey('dashboard', 'private-widget', 'get', { - params: computed(() => ({ - widget_id: widgetId?.value as string, - })), - }); - const { - key: publicDataTableListQueryKey, - params: publicDataTableListParams, - } = _useAPIQueryKey('dashboard', 'public-data-table', 'list', { - params: computed(() => ({ - widget_id: widgetId?.value as string, - })), - }); - const { - key: privateDataTableListQueryKey, - params: privateDataTableListParams, - } = _useAPIQueryKey('dashboard', 'private-data-table', 'list', { - params: computed(() => ({ - widget_id: widgetId?.value as string, - })), - }); - + const _publicWidgetGetQueryKey = computed(() => [ + ...publicWidgetGetQueryKey.value, + widgetId?.value, + ]); + const _privateWidgetGetQueryKey = computed(() => [ + ...privateWidgetGetQueryKey.value, + widgetId?.value, + ]); + const _publicDataTableListQueryKey = computed(() => [ + ...publicDataTableListQueryKey.value, + widgetId?.value, + ]); + const _privateDataTableListQueryKey = computed(() => [ + ...privateDataTableListQueryKey.value, + widgetId?.value, + ]); /* Querys */ const publicWidgetQuery = useScopedQuery({ - queryKey: publicWidgetGetQueryKey, - queryFn: () => publicWidgetAPI.get(publicWidgetGetParams.value), + queryKey: _publicWidgetGetQueryKey, + queryFn: () => publicWidgetAPI.get({ + widget_id: widgetId?.value as string, + }), enabled: computed(() => !!widgetId?.value && !isPrivate.value && !preventLoad), staleTime: STALE_TIME, }, ['DOMAIN', 'WORKSPACE']); const privateWidgetQuery = useScopedQuery({ - queryKey: privateWidgetGetQueryKey, - queryFn: () => privateWidgetAPI.get(privateWidgetGetParams.value), + queryKey: _privateWidgetGetQueryKey, + queryFn: () => privateWidgetAPI.get({ + widget_id: widgetId?.value as string, + }), enabled: computed(() => !!widgetId?.value && isPrivate.value && !preventLoad), staleTime: STALE_TIME, }, ['WORKSPACE']); const publicDataTableListQuery = useScopedQuery({ - queryKey: publicDataTableListQueryKey, - queryFn: () => publicDataTableAPI.list(publicDataTableListParams.value), + queryKey: _publicDataTableListQueryKey, + queryFn: () => publicDataTableAPI.list({ + widget_id: widgetId?.value as string, + }), select: (data) => data?.results || [], enabled: computed(() => !!widgetId?.value && !isPrivate.value && !preventLoad), initialData: DEFAULT_LIST_DATA, @@ -123,8 +130,10 @@ export const useWidgetFormQuery = ({ staleTime: STALE_TIME, }, ['DOMAIN', 'WORKSPACE']); const privateDataTableListQuery = useScopedQuery({ - queryKey: privateDataTableListQueryKey, - queryFn: () => privateDataTableAPI.list(privateDataTableListParams.value), + queryKey: _privateDataTableListQueryKey, + queryFn: () => privateDataTableAPI.list({ + widget_id: widgetId?.value as string, + }), select: (data) => data?.results || [], enabled: computed(() => !!widgetId?.value && isPrivate.value && !preventLoad), initialData: DEFAULT_LIST_DATA, @@ -166,10 +175,16 @@ export const useWidgetFormQuery = ({ privateDataTableAPI, }, keys: { - publicWidgetGetQueryKey, - privateWidgetGetQueryKey, - publicDataTableListQueryKey, - privateDataTableListQueryKey, + publicWidgetGetQueryKey: _publicWidgetGetQueryKey, + privateWidgetGetQueryKey: _privateWidgetGetQueryKey, + publicDataTableListQueryKey: _publicDataTableListQueryKey, + privateDataTableListQueryKey: _privateDataTableListQueryKey, + publicWidgetLoadQueryKey: computed(() => publicWidgetLoadQueryKey.value), + privateWidgetLoadQueryKey: computed(() => privateWidgetLoadQueryKey.value), + publicWidgetLoadSumQueryKey: computed(() => publicWidgetLoadSumQueryKey.value), + privateWidgetLoadSumQueryKey: computed(() => privateWidgetLoadSumQueryKey.value), + publicDataTableLoadQueryKey: computed(() => publicDataTableLoadQueryKey.value), + privateDataTableLoadQueryKey: computed(() => privateDataTableLoadQueryKey.value), }, fetcher: { updateDataTableFn, diff --git a/apps/web/src/common/modules/widgets/_widgets/table/Table.vue b/apps/web/src/common/modules/widgets/_widgets/table/Table.vue index 5a4cb2e37d..80cd62a945 100644 --- a/apps/web/src/common/modules/widgets/_widgets/table/Table.vue +++ b/apps/web/src/common/modules/widgets/_widgets/table/Table.vue @@ -10,7 +10,6 @@ import type { Sort } from '@cloudforet/core-lib/space-connector/type'; import { PPagination } from '@cloudforet/mirinae'; import type { WidgetLoadParams, WidgetLoadResponse, WidgetLoadSumParams } from '@/api-clients/dashboard/_types/widget-type'; -import { _useAPIQueryKey } from '@/query/composables/use-api-query-key'; import { i18n } from '@/translations'; import ErrorHandler from '@/common/composables/error/errorHandler'; @@ -21,6 +20,9 @@ import { useWidgetFrame } from '@/common/modules/widgets/_composables/use-widget import { DATA_TABLE_OPERATOR } from '@/common/modules/widgets/_constants/data-table-constant'; import { WIDGET_LOAD_STALE_TIME } from '@/common/modules/widgets/_constants/widget-constant'; import { SUB_TOTAL_NAME } from '@/common/modules/widgets/_constants/widget-field-constant'; +import { + normalizeAndSerializeVars, +} from '@/common/modules/widgets/_helpers/global-variable-helper'; import type { CustomTableColumnWidthValue } from '@/common/modules/widgets/_widget-fields/custom-table-column-width/type'; import type { DataFieldHeatmapColorValue } from '@/common/modules/widgets/_widget-fields/data-field-heatmap-color/type'; import type { DataFieldValue } from '@/common/modules/widgets/_widget-fields/data-field/type'; @@ -51,83 +53,10 @@ const REFERENCE_FIELDS = ['Project', 'Workspace', 'Region', 'Service Account']; const props = defineProps(); const emit = defineEmits(); -const { api } = useWidgetFormQuery({ +const { keys, api } = useWidgetFormQuery({ widgetId: computed(() => props.widgetId), preventLoad: true, }); -const { key: publicWidgetLoadQueryKey, params: publicWidgetLoadParams } = _useAPIQueryKey('dashboard', 'public-widget', 'load', { - id: computed(() => props.widgetId), - params: computed(() => ({ - widget_id: props.widgetId, - start: dateRange.value.start, - end: dateRange.value.end, - sort: getTableDefaultSortBy(state.sortBy), - page: { - start: (state.pageSize * (state.thisPage - 1)) + 1, - limit: state.pageSize, - }, - group_by: (widgetOptionsState.groupByInfo?.data as string[]) ?? [], - vars: props.dashboardVars, - granularity: widgetOptionsState.granularityInfo?.granularity, - })), - deps: computed(() => ({ - widgetName: props.widgetName, - dataTableId: props.dataTableId, - })), -}); - -const { key: privateWidgetLoadQueryKey, params: privateWidgetLoadParams } = _useAPIQueryKey('dashboard', 'private-widget', 'load', { - id: computed(() => props.widgetId), - params: computed(() => ({ - widget_id: props.widgetId, - start: dateRange.value.start, - end: dateRange.value.end, - sort: getTableDefaultSortBy(state.sortBy), - page: { - start: (state.pageSize * (state.thisPage - 1)) + 1, - limit: state.pageSize, - }, - group_by: (widgetOptionsState.groupByInfo?.data as string[]) ?? [], - vars: props.dashboardVars, - granularity: widgetOptionsState.granularityInfo?.granularity, - })), - deps: computed(() => ({ - widgetName: props.widgetName, - dataTableId: props.dataTableId, - })), -}); - -const { key: publicWidgetLoadSumQueryKey, params: publicWidgetLoadSumParams } = _useAPIQueryKey('dashboard', 'public-widget', 'load-sum', { - id: computed(() => props.widgetId), - params: computed(() => ({ - widget_id: props.widgetId, - start: dateRange.value.start, - end: dateRange.value.end, - vars: props.dashboardVars, - granularity: widgetOptionsState.granularityInfo?.granularity, - })), - deps: computed(() => ({ - widgetName: props.widgetName, - dataTableId: props.dataTableId, - enabledTotal: !!widgetOptionsState.totalInfo?.toggleValue, - })), -}); - -const { key: privateWidgetLoadSumQueryKey, params: privateWidgetLoadSumParams } = _useAPIQueryKey('dashboard', 'private-widget', 'load-sum', { - id: computed(() => props.widgetId), - params: computed(() => ({ - widget_id: props.widgetId, - start: dateRange.value.start, - end: dateRange.value.end, - vars: props.dashboardVars, - granularity: widgetOptionsState.granularityInfo?.granularity, - })), - deps: computed(() => ({ - widgetName: props.widgetName, - dataTableId: props.dataTableId, - enabledTotal: !!widgetOptionsState.totalInfo?.toggleValue, - })), -}); const { dateRange } = useWidgetDateRange({ dateRangeFieldValue: computed(() => (props.widgetOptions?.dateRange?.value as DateRangeValue)), @@ -229,11 +158,60 @@ const fetchWidgetSumData = async (params: WidgetLoadSumParams): Promise [ + ...(state.isPrivateWidget ? keys.privateWidgetLoadQueryKey.value : keys.publicWidgetLoadQueryKey.value), + props.dashboardId, + props.widgetId, + props.widgetName, + { + start: dateRange.value.start, + end: dateRange.value.end, + sort: state.sortBy, + page: state.thisPage, + pageSize: state.pageSize, + granularity: widgetOptionsState.granularityInfo?.granularity, + groupBy: widgetOptionsState.groupByInfo?.data, + dataTableId: props.dataTableId, + // dataTableOptions: normalizeAndSerializeDataTableOptions(state.dataTable?.options || {}), + // dataTables: normalizeAndSerializeDataTableOptions((props.dataTables || []).map((d) => d?.options || {})), + vars: normalizeAndSerializeVars(props.dashboardVars), + }, +]); + +const fullDataQueryKey = computed(() => [ + ...(state.isPrivateWidget ? keys.privateWidgetLoadSumQueryKey.value : keys.publicWidgetLoadSumQueryKey.value), + props.dashboardId, + props.widgetId, + props.widgetName, + { + start: dateRange.value.start, + end: dateRange.value.end, + granularity: widgetOptionsState.granularityInfo?.granularity, + dataTableId: props.dataTableId, + // dataTableOptions: normalizeAndSerializeDataTableOptions(state.dataTable?.options || {}), + // dataTables: normalizeAndSerializeDataTableOptions((props.dataTables || []).map((d) => d?.options || {})), + enabledTotal: !!widgetOptionsState.totalInfo?.toggleValue, + vars: normalizeAndSerializeVars(props.dashboardVars), + }, +]); + const queryResults = useQueries({ queries: [ { - queryKey: state.isPrivateWidget ? privateWidgetLoadQueryKey : publicWidgetLoadQueryKey, - queryFn: () => fetchWidgetData(state.isPrivateWidget ? privateWidgetLoadParams.value : publicWidgetLoadParams.value), + queryKey: baseQueryKey, + queryFn: () => fetchWidgetData({ + widget_id: props.widgetId, + start: dateRange.value.start, + end: dateRange.value.end, + sort: getTableDefaultSortBy(state.sortBy), + page: { + start: (state.pageSize * (state.thisPage - 1)) + 1, + limit: state.pageSize, + }, + group_by: (widgetOptionsState.groupByInfo?.data as string[]) ?? [], + vars: props.dashboardVars, + granularity: widgetOptionsState.granularityInfo?.granularity, + }), enabled: computed(() => { const widgetActive = props.widgetState !== 'INACTIVE'; const dataTableReady = !!state.dataTable; @@ -243,8 +221,14 @@ const queryResults = useQueries({ staleTime: WIDGET_LOAD_STALE_TIME, }, { - queryKey: state.isPrivateWidget ? privateWidgetLoadSumQueryKey : publicWidgetLoadSumQueryKey, - queryFn: () => fetchWidgetSumData(state.isPrivateWidget ? privateWidgetLoadSumParams.value : publicWidgetLoadSumParams.value), + queryKey: fullDataQueryKey, + queryFn: () => fetchWidgetSumData({ + widget_id: props.widgetId, + start: dateRange.value.start, + end: dateRange.value.end, + vars: props.dashboardVars, + granularity: widgetOptionsState.granularityInfo?.granularity, + }), enabled: computed(() => { const widgetActive = props.widgetState !== 'INACTIVE'; const dataTableReady = !!state.dataTable; diff --git a/apps/web/src/services/dashboards/composables/use-dashboard-detail-query.ts b/apps/web/src/services/dashboards/composables/use-dashboard-detail-query.ts index b7f7210fb7..9049e25f79 100644 --- a/apps/web/src/services/dashboards/composables/use-dashboard-detail-query.ts +++ b/apps/web/src/services/dashboards/composables/use-dashboard-detail-query.ts @@ -8,28 +8,23 @@ import { useScopedQuery } from '@/api-clients/_common/composables/use-scoped-que import type { DashboardModel, DashboardUpdateParams } from '@/api-clients/dashboard/_types/dashboard-type'; import type { WidgetModel, WidgetUpdateParams } from '@/api-clients/dashboard/_types/widget-type'; import { usePrivateDashboardApi } from '@/api-clients/dashboard/private-dashboard/composables/use-private-dashboard-api'; -import type { PrivateDashboardGetParameters } from '@/api-clients/dashboard/private-dashboard/schema/api-verbs/get'; import type { PrivateDashboardUpdateParameters } from '@/api-clients/dashboard/private-dashboard/schema/api-verbs/update'; import type { PrivateDashboardModel } from '@/api-clients/dashboard/private-dashboard/schema/model'; import { usePrivateWidgetApi } from '@/api-clients/dashboard/private-widget/composables/use-private-widget-api'; -import type { PrivateWidgetListParameters } from '@/api-clients/dashboard/private-widget/schema/api-verbs/list'; import type { PrivateWidgetUpdateParameters } from '@/api-clients/dashboard/private-widget/schema/api-verbs/update'; import type { PrivateWidgetModel } from '@/api-clients/dashboard/private-widget/schema/model'; import { usePublicDashboardApi } from '@/api-clients/dashboard/public-dashboard/composables/use-public-dashboard-api'; -import type { PublicDashboardGetParameters } from '@/api-clients/dashboard/public-dashboard/schema/api-verbs/get'; 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 { usePublicWidgetApi } from '@/api-clients/dashboard/public-widget/composables/use-public-widget-api'; -import type { PublicWidgetListParameters } from '@/api-clients/dashboard/public-widget/schema/api-verbs/list'; import type { PublicWidgetUpdateParameters } from '@/api-clients/dashboard/public-widget/schema/api-verbs/update'; import type { PublicWidgetModel } from '@/api-clients/dashboard/public-widget/schema/model'; -import { _useAPIQueryKey } from '@/query/composables/use-api-query-key'; const DEFAULT_LIST_DATA = { results: [] }; const STALE_TIME = 1000 * 60 * 5; interface UseDashboardDetailQueryOptions { - dashboardId: ComputedRef; + dashboardId: ComputedRef; } interface UseDashboardDetailQueryReturn { @@ -59,46 +54,54 @@ interface UseDashboardDetailQueryReturn { export const useDashboardDetailQuery = ({ dashboardId, }: UseDashboardDetailQueryOptions): UseDashboardDetailQueryReturn => { - const { publicDashboardAPI } = usePublicDashboardApi(); - const { privateDashboardAPI } = usePrivateDashboardApi(); - const { publicWidgetAPI } = usePublicWidgetApi(); - const { privateWidgetAPI } = usePrivateWidgetApi(); + const { publicDashboardAPI, publicDashboardGetQueryKey } = usePublicDashboardApi(); + const { privateDashboardAPI, privateDashboardGetQueryKey } = usePrivateDashboardApi(); + const { publicWidgetAPI, publicWidgetListQueryKey } = usePublicWidgetApi(); + const { privateWidgetAPI, privateWidgetListQueryKey } = usePrivateWidgetApi(); const queryClient = useQueryClient(); const isPrivate = computed(() => !!dashboardId.value?.startsWith('private')); /* Query Keys */ - const { key: publicDashboardGetQueryKey, params: publicDashboardGetParams } = _useAPIQueryKey('dashboard', 'public-dashboard', 'get', { - id: dashboardId, - params: computed(() => ({ dashboard_id: dashboardId.value })), - }); - const { key: privateDashboardGetQueryKey, params: privateDashboardGetParams } = _useAPIQueryKey('dashboard', 'private-dashboard', 'get', { - id: dashboardId, - params: computed(() => ({ dashboard_id: dashboardId.value })), - }); - const { key: publicWidgetListQueryKey, params: publicWidgetListParams } = _useAPIQueryKey('dashboard', 'public-widget', 'list', { - params: computed(() => ({ dashboard_id: dashboardId.value })), - }); - const { key: privateWidgetListQueryKey, params: privateWidgetListParams } = _useAPIQueryKey('dashboard', 'private-widget', 'list', { - params: computed(() => ({ dashboard_id: dashboardId.value })), - }); + const _publicDashboardGetQueryKey = computed(() => [ + ...publicDashboardGetQueryKey.value, + dashboardId.value, + ]); + const _privateDashboardGetQueryKey = computed(() => [ + ...privateDashboardGetQueryKey.value, + dashboardId.value, + ]); + const _publicWidgetListQueryKey = computed(() => [ + ...publicWidgetListQueryKey.value, + dashboardId.value, + ]); + const _privateWidgetListQueryKey = computed(() => [ + ...privateWidgetListQueryKey.value, + dashboardId.value, + ]); /* Querys */ const publicDashboardQuery = useScopedQuery({ - queryKey: publicDashboardGetQueryKey, - queryFn: () => publicDashboardAPI.get(publicDashboardGetParams.value), + queryKey: _publicDashboardGetQueryKey, + queryFn: () => publicDashboardAPI.get({ + dashboard_id: dashboardId.value as string, + }), enabled: computed(() => !!dashboardId.value && !isPrivate.value), staleTime: STALE_TIME, }, ['DOMAIN', 'WORKSPACE']); const privateDashboardQuery = useScopedQuery({ - queryKey: privateDashboardGetQueryKey, - queryFn: () => privateDashboardAPI.get(privateDashboardGetParams.value), + queryKey: _privateDashboardGetQueryKey, + queryFn: () => privateDashboardAPI.get({ + dashboard_id: dashboardId.value as string, + }), enabled: computed(() => !!dashboardId.value && isPrivate.value), staleTime: STALE_TIME, }, ['WORKSPACE']); const publicWidgetListQuery = useScopedQuery({ - queryKey: publicWidgetListQueryKey, - queryFn: () => publicWidgetAPI.list(publicWidgetListParams.value), + queryKey: _publicWidgetListQueryKey, + queryFn: () => publicWidgetAPI.list({ + dashboard_id: dashboardId.value as string, + }), select: (data) => data?.results || [], enabled: computed(() => !!dashboardId.value && publicDashboardQuery.isSuccess && !isPrivate.value), initialData: DEFAULT_LIST_DATA, @@ -106,8 +109,10 @@ export const useDashboardDetailQuery = ({ staleTime: STALE_TIME, }, ['DOMAIN', 'WORKSPACE']); const privateWidgetListQuery = useScopedQuery({ - queryKey: privateWidgetListQueryKey, - queryFn: () => privateWidgetAPI.list(privateWidgetListParams.value), + queryKey: _privateWidgetListQueryKey, + queryFn: () => privateWidgetAPI.list({ + dashboard_id: dashboardId.value as string, + }), select: (data) => data?.results || [], enabled: computed(() => !!dashboardId.value && privateDashboardQuery.isSuccess && isPrivate.value), initialData: DEFAULT_LIST_DATA, @@ -151,10 +156,10 @@ export const useDashboardDetailQuery = ({ privateWidgetAPI, }, keys: { - publicDashboardGetQueryKey, - privateDashboardGetQueryKey, - publicWidgetListQueryKey, - privateWidgetListQueryKey, + publicDashboardGetQueryKey: _publicDashboardGetQueryKey, + privateDashboardGetQueryKey: _privateDashboardGetQueryKey, + publicWidgetListQueryKey: _publicWidgetListQueryKey, + privateWidgetListQueryKey: _privateWidgetListQueryKey, }, fetcher: { updateDashboardFn, diff --git a/apps/web/src/services/dashboards/composables/use-dashboard-query.ts b/apps/web/src/services/dashboards/composables/use-dashboard-query.ts index 4def300a75..cd1bf71229 100644 --- a/apps/web/src/services/dashboards/composables/use-dashboard-query.ts +++ b/apps/web/src/services/dashboards/composables/use-dashboard-query.ts @@ -3,6 +3,7 @@ import { computed, type ComputedRef, reactive, watch, } from 'vue'; +import type { QueryKey } from '@tanstack/vue-query'; import { type QueryClient, useQueryClient } from '@tanstack/vue-query'; import { ApiQueryHelper } from '@cloudforet/core-lib/space-connector/helper'; @@ -20,8 +21,6 @@ import type { PublicDashboardModel } from '@/api-clients/dashboard/public-dashbo import { usePublicFolderApi } from '@/api-clients/dashboard/public-folder/composables/use-public-folder-api'; import type { PublicFolderUpdateParameters } from '@/api-clients/dashboard/public-folder/schema/api-verbs/update'; import type { PublicFolderModel } from '@/api-clients/dashboard/public-folder/schema/model'; -import type { QueryKeyArray } from '@/query/_types/query-key-type'; -import { _useAPIQueryKey } from '@/query/composables/use-api-query-key'; import { useAppContextStore } from '@/store/app-context/app-context-store'; import { useUserWorkspaceStore } from '@/store/app-context/workspace/user-workspace-store'; @@ -39,10 +38,10 @@ interface UseDashboardQueryReturn { privateFolderList: Ref; isLoading: ComputedRef; keys: { - publicDashboardListQueryKey: ComputedRef; - privateDashboardListQueryKey: ComputedRef; - publicFolderListQueryKey: ComputedRef; - privateFolderListQueryKey: ComputedRef; + publicDashboardListQueryKey: ComputedRef; + privateDashboardListQueryKey: ComputedRef; + publicFolderListQueryKey: ComputedRef; + privateFolderListQueryKey: ComputedRef; }; fetcher: { updateFolderFn: (args: FolderUpdateParams) => Promise @@ -57,11 +56,10 @@ interface UseDashboardQueryReturn { } export const useDashboardQuery = (): UseDashboardQueryReturn => { - const { publicDashboardAPI } = usePublicDashboardApi(); - const { privateDashboardAPI } = usePrivateDashboardApi(); - const { publicFolderAPI } = usePublicFolderApi(); - const { privateFolderAPI } = usePrivateFolderApi(); - + const { publicDashboardAPI, publicDashboardListQueryKey } = usePublicDashboardApi(); + const { privateDashboardAPI, privateDashboardListQueryKey } = usePrivateDashboardApi(); + const { publicFolderAPI, publicFolderListQueryKey } = usePublicFolderApi(); + const { privateFolderAPI, privateFolderListQueryKey } = usePrivateFolderApi(); const queryClient = useQueryClient(); @@ -88,44 +86,52 @@ export const useDashboardQuery = (): UseDashboardQueryReturn => { }, }); - /* Query Keys and Params */ - const { key: publicDashboardListQueryKey, params: publicDashboardListParams } = _useAPIQueryKey('dashboard', 'public-dashboard', 'list', { - params: computed(() => ({ - query: { - ..._state.publicDashboardListApiQuery, - ..._state.defaultListQuery.query, - }, - })), - }); - const { key: privateDashboardListQueryKey, params: privateDashboardListParams } = _useAPIQueryKey('dashboard', 'private-dashboard', 'list', { - params: computed(() => _state.defaultListQuery), - }); - const { key: publicFolderListQueryKey, params: publicFolderListParams } = _useAPIQueryKey('dashboard', 'public-folder', 'list', { - params: computed(() => ({ - query: { - ..._state.publicFolderListApiQuery, - ..._state.defaultListQuery.query, - }, - })), - }); - const { key: privateFolderListQueryKey, params: privateFolderListParams } = _useAPIQueryKey('dashboard', 'private-folder', 'list', { - params: computed(() => _state.defaultListQuery), - }); + /* Query Keys */ + const _publicDashboardListQueryKey = computed(() => [ + ...publicDashboardListQueryKey.value, + _state.defaultListQuery, + _state.publicDashboardListApiQuery.filter, + ]); + const _privateDashboardListQueryKey = computed(() => [ + ...privateDashboardListQueryKey.value, + _state.defaultListQuery, + ]); + const _publicFolderListQueryKey = computed(() => [ + ...publicFolderListQueryKey.value, + _state.defaultListQuery, + _state.publicFolderListApiQuery.filter, + ]); + const _privateFolderListQueryKey = computed(() => [ + ...privateFolderListQueryKey.value, + _state.defaultListQuery, + ]); + /* Querys */ const publicDashboardListQuery = useScopedQuery, unknown, PublicDashboardModel[]>({ - queryKey: publicDashboardListQueryKey, - queryFn: () => publicDashboardAPI.list(publicDashboardListParams.value), + queryKey: _publicDashboardListQueryKey, + queryFn: () => publicDashboardAPI.list({ + query: { + ..._state.publicDashboardListApiQuery, + sort: [{ + key: 'created_at', + desc: true, + }], + }, + }), select: (data) => data?.results ?? [], initialData: DEFAULT_LIST_DATA, initialDataUpdatedAt: 0, staleTime: STALE_TIME, enabled: computed(() => !!_state.publicDashboardListApiQuery?.filter), }, ['DOMAIN', 'WORKSPACE']); - const privateDashboardListQuery = useScopedQuery, unknown, PrivateDashboardModel[]>({ - queryKey: privateDashboardListQueryKey, - queryFn: () => privateDashboardAPI.list(privateDashboardListParams.value), + queryKey: _privateDashboardListQueryKey, + queryFn: () => privateDashboardAPI.list({ + query: { + sort: [{ key: 'created_at', desc: true }], + }, + }), select: (data) => data?.results || [], initialData: DEFAULT_LIST_DATA, initialDataUpdatedAt: 0, @@ -133,8 +139,13 @@ export const useDashboardQuery = (): UseDashboardQueryReturn => { enabled: computed(() => !_state.isAdminMode), }, ['WORKSPACE']); const publicFolderListQuery = useScopedQuery, unknown, PublicFolderModel[]>({ - queryKey: publicFolderListQueryKey, - queryFn: () => publicFolderAPI.list(publicFolderListParams.value), + queryKey: _publicFolderListQueryKey, + queryFn: () => publicFolderAPI.list({ + query: { + ..._state.publicFolderListApiQuery, + sort: [{ key: 'created_at', desc: true }], + }, + }), select: (data) => data?.results || [], initialData: DEFAULT_LIST_DATA, initialDataUpdatedAt: 0, @@ -142,8 +153,12 @@ export const useDashboardQuery = (): UseDashboardQueryReturn => { enabled: computed(() => !!_state.publicFolderListApiQuery?.filter), }, ['DOMAIN', 'WORKSPACE']); const privateFolderListQuery = useScopedQuery, unknown, PrivateFolderModel[]>({ - queryKey: privateFolderListQueryKey, - queryFn: () => privateFolderAPI.list(privateFolderListParams.value), + queryKey: _privateFolderListQueryKey, + queryFn: () => privateFolderAPI.list({ + query: { + sort: [{ key: 'created_at', desc: true }], + }, + }), select: (data) => data?.results || [], initialData: DEFAULT_LIST_DATA, initialDataUpdatedAt: 0, @@ -184,10 +199,10 @@ export const useDashboardQuery = (): UseDashboardQueryReturn => { privateFolderList: computed(() => privateFolderListQuery.data.value ?? []), isLoading, keys: { - publicDashboardListQueryKey, - privateDashboardListQueryKey, - publicFolderListQueryKey, - privateFolderListQueryKey, + publicDashboardListQueryKey: _publicDashboardListQueryKey, + privateDashboardListQueryKey: _privateDashboardListQueryKey, + publicFolderListQueryKey: _publicFolderListQueryKey, + privateFolderListQueryKey: _privateFolderListQueryKey, }, fetcher: { updateFolderFn, From 3f4e8d6882edf7dc649623a4cd2ee55311028315 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=9A=A9=ED=83=9C?= Date: Wed, 26 Mar 2025 19:14:25 +0900 Subject: [PATCH 08/17] fix(query-key): refactor api query-key composable Signed-off-by: piggggggggy --- .../src/query/query-key/use-api-query-key.md | 6 +-- .../src/query/query-key/use-api-query-key.ts | 52 +++++++++++-------- 2 files changed, 34 insertions(+), 24 deletions(-) diff --git a/apps/web/src/query/query-key/use-api-query-key.md b/apps/web/src/query/query-key/use-api-query-key.md index 7eca13f70c..f6fc3f659d 100644 --- a/apps/web/src/query/query-key/use-api-query-key.md +++ b/apps/web/src/query/query-key/use-api-query-key.md @@ -23,7 +23,7 @@ This predictable structure allows for precise cache management and query invalid resource, // resource name (e.g., 'public-data-table') verb, // action (e.g., 'get', 'list', 'load') id?, // (optional) resource identifier (with 'get' verb) - params, // request parameters + params?, // (optional) request parameters deps? // (optional) dependencies ] ``` @@ -33,7 +33,7 @@ This predictable structure allows for precise cache management and query invalid ```typescript interface UseAPIQueryKeyOptions

{ id?: _MaybeRefOrGetter; - params: _MaybeRefOrGetter

; + params?: _MaybeRefOrGetter

; deps?: _MaybeRefOrGetter; } ``` @@ -43,7 +43,7 @@ interface UseAPIQueryKeyOptions

{ ```typescript interface UseAPIQueryKeyResult

{ key: ComputedRef; // query key - params: ComputedRef

; // parameters + params?: ComputedRef

; // parameters deps?: ComputedRef; // dependencies (optional) id?: ComputedRef; // ID (optional) } diff --git a/apps/web/src/query/query-key/use-api-query-key.ts b/apps/web/src/query/query-key/use-api-query-key.ts index 238b101835..25197bc6e7 100644 --- a/apps/web/src/query/query-key/use-api-query-key.ts +++ b/apps/web/src/query/query-key/use-api-query-key.ts @@ -8,10 +8,12 @@ import type { QueryKeyArray, ResourceName, ServiceName, Verb, } from '@/query/query-key/_types/query-key-type'; +// Cache for debug logs +// const debugLogCache = new Map(); +// const DEBUG_LOG_THROTTLE = 1000; // 1초 type _MaybeRefOrGetter = T | Ref | (() => T); - /** * Options for generating API query keys. * @@ -25,7 +27,7 @@ type _MaybeRefOrGetter = T | Ref | (() => T); * resource, * verb, * id?, // Optional, appears first in namespace if present - * params, // Required, always present + * params?, // Optional, appears second if present * deps? // Optional, appears last if present * ] * @@ -35,28 +37,28 @@ type _MaybeRefOrGetter = T | Ref | (() => T); * @property id - Optional identifier for single resource operations (e.g., get, load). * When present, it appears first in the namespace part of the query key, * enabling hierarchical cache management for single-resource operations. - * @property params - Required parameters for the API request. + * @property params - Optional parameters for the API request. * @property deps - Optional dependencies that affect the query key. */ -interface UseAPIQueryKeyOptions

{ +interface UseAPIQueryKeyOptions { id?: _MaybeRefOrGetter; - params: _MaybeRefOrGetter

; + params?: _MaybeRefOrGetter; deps?: _MaybeRefOrGetter; } -interface UseAPIQueryKeyResult

{ +type UseAPIQueryKeyResult = { key: ComputedRef; - params: ComputedRef

; + params: T extends undefined ? undefined : ComputedRef; deps?: ComputedRef; id?: ComputedRef; -} +}; -export const _useAPIQueryKey = , V extends Verb, P extends object>( +export const _useAPIQueryKey = , V extends Verb, T extends object = object>( service: S, resource: R, verb: V, - options: UseAPIQueryKeyOptions

, -): UseAPIQueryKeyResult

=> { + options: UseAPIQueryKeyOptions, +): UseAPIQueryKeyResult => { // Runtime validation for development environment if (import.meta.env.DEV) { if (!service || !resource || !verb) { @@ -79,30 +81,27 @@ export const _useAPIQueryKey = ...globalContext.value, service, resource, verb, ...(resolvedId ? [resolvedId] : []), - createImmutableObjectKeyItem(resolvedParams), + ...(resolvedParams ? [createImmutableObjectKeyItem(resolvedParams)] : []), ...(resolvedDeps ? [createImmutableObjectKeyItem(resolvedDeps)] : []), ]; }); - // NOTE: Only for development environment. After using tanstack query devtools, this will be removed. - if (import.meta.env.DEV) { - console.debug(`[QueryKey] ${String(service)}/${String(resource)}/${String(verb)}`, JSON.stringify(queryKey.value, null, 2)); - } + // if (import.meta.env.DEV) { + // _logQueryKeyDebug(queryKey.value); + // } return { key: queryKey, - params: computed(() => toValue(params)), + params: params ? computed(() => toValue(params)) : undefined, deps: deps ? computed(() => toValue(deps)) : undefined, id: id ? computed(() => toValue(id)) : undefined, - }; + } as UseAPIQueryKeyResult; }; - - const _validateQueryKeyOptions =

(options: { id?: _MaybeRefOrGetter; - params: _MaybeRefOrGetter

; + params?: _MaybeRefOrGetter

; deps?: _MaybeRefOrGetter; }) => { if (options.params) { @@ -126,3 +125,14 @@ const _validateQueryKeyOptions =

(options: { } } }; + +// const _logQueryKeyDebug = (queryKey: QueryKeyArray) => { +// const now = Date.now(); +// const key = queryKey.join('/'); +// const lastLogTime = debugLogCache.get(key) || 0; + +// if (now - lastLogTime >= DEBUG_LOG_THROTTLE) { +// console.debug('[QueryKey]', { queryKey }); +// debugLogCache.set(key, now); +// } +// }; From aa9e0d6d720d2494dfd69b1d3122f170193f7674 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=9A=A9=ED=83=9C?= Date: Wed, 26 Mar 2025 22:36:31 +0900 Subject: [PATCH 09/17] chore(query-key): edit snapshot test result Signed-off-by: piggggggggy --- .../src/query/query-key/__tests__/use-api-query-key.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/src/query/query-key/__tests__/use-api-query-key.test.ts b/apps/web/src/query/query-key/__tests__/use-api-query-key.test.ts index 8fa4ee4318..0c28d4b242 100644 --- a/apps/web/src/query/query-key/__tests__/use-api-query-key.test.ts +++ b/apps/web/src/query/query-key/__tests__/use-api-query-key.test.ts @@ -153,7 +153,7 @@ describe('_useAPIQueryKey', () => { }, { "filter": "active", - } + }, ] `); }); @@ -191,7 +191,7 @@ describe('_useAPIQueryKey', () => { }, { "filter": "active", - } + }, ] `); }); From ceaf2e35e05444ec600bab3fd1217cf8bdfa5414 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=9A=A9=ED=83=9C?= Date: Wed, 26 Mar 2025 23:12:18 +0900 Subject: [PATCH 10/17] feat(api-query-key): add `namespaces` return value to composable Signed-off-by: piggggggggy --- .../src/query/query-key/use-api-query-key.md | 24 ++++++++++++++++++- .../src/query/query-key/use-api-query-key.ts | 9 ++++++- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/apps/web/src/query/query-key/use-api-query-key.md b/apps/web/src/query/query-key/use-api-query-key.md index f6fc3f659d..3467096735 100644 --- a/apps/web/src/query/query-key/use-api-query-key.md +++ b/apps/web/src/query/query-key/use-api-query-key.md @@ -42,7 +42,8 @@ interface UseAPIQueryKeyOptions

{ ```typescript interface UseAPIQueryKeyResult

{ - key: ComputedRef; // query key + key: ComputedRef; // full query key + namespaces: ComputedRef; // hierarchical structure for cache management params?: ComputedRef

; // parameters deps?: ComputedRef; // dependencies (optional) id?: ComputedRef; // ID (optional) @@ -108,11 +109,32 @@ const { data } = useQuery({ }); ``` +### Cache Invalidation +```typescript +const { key, namespaces, params } = _useAPIQueryKey( + 'dashboard', + 'public-data-table', + 'get', + { + id: toRef(state, 'currentDataTableId') + params: computed(() => state.params) + deps: computed(() => ({ filter: 'active' }), + } +); + +// Invalidate specific query +queryClient.invalidateQueries({ queryKey: key.value }); + +// Invalidate all queries under a namespace +queryClient.invalidateQueries({ queryKey: [...namespaces.value, state.currentDataTableId] }); +``` + ### Best Practices 1. Always type your params interface for better type safety 2. Use the returned `params` in your `queryFn` to maintain consistency 3. Leverage TypeScript's type inference for better development experience 4. Consider using `deps` for values that should trigger cache invalidation but aren't part of the API request +5. Use `namespaces` for hierarchical cache invalidation when needed ## Maintenance Guide diff --git a/apps/web/src/query/query-key/use-api-query-key.ts b/apps/web/src/query/query-key/use-api-query-key.ts index 25197bc6e7..971515bcdc 100644 --- a/apps/web/src/query/query-key/use-api-query-key.ts +++ b/apps/web/src/query/query-key/use-api-query-key.ts @@ -48,6 +48,7 @@ interface UseAPIQueryKeyOptions { type UseAPIQueryKeyResult = { key: ComputedRef; + namespaces: ComputedRef; params: T extends undefined ? undefined : ComputedRef; deps?: ComputedRef; id?: ComputedRef; @@ -57,7 +58,7 @@ export const _useAPIQueryKey = service: S, resource: R, verb: V, - options: UseAPIQueryKeyOptions, + options: UseAPIQueryKeyOptions = {}, ): UseAPIQueryKeyResult => { // Runtime validation for development environment if (import.meta.env.DEV) { @@ -86,6 +87,11 @@ export const _useAPIQueryKey = ]; }); + const namespaces = computed(() => [ + ...globalContext.value, + service, resource, verb, + ]); + // NOTE: Only for development environment. After using tanstack query devtools, this will be removed. // if (import.meta.env.DEV) { // _logQueryKeyDebug(queryKey.value); @@ -93,6 +99,7 @@ export const _useAPIQueryKey = return { key: queryKey, + namespaces, params: params ? computed(() => toValue(params)) : undefined, deps: deps ? computed(() => toValue(deps)) : undefined, id: id ? computed(() => toValue(id)) : undefined, From ddbdd5250bc3443c203075d7023a5125648f4e40 Mon Sep 17 00:00:00 2001 From: Yongtae Park Date: Thu, 27 Mar 2025 14:33:12 +0900 Subject: [PATCH 11/17] feat(service-query-key): apply service query-key review (improvement & optimization) Signed-off-by: samuel.park --- ....test.ts => use-service-query-key.test.ts} | 106 ++-- .../_helpers/immutable-query-key-helper.ts | 2 +- .../src/query/query-key/use-api-query-key.md | 167 ------ .../src/query/query-key/use-api-query-key.ts | 145 ------ .../query/query-key/use-service-query-key.md | 492 ++++++++++++++++++ .../query/query-key/use-service-query-key.ts | 136 +++++ 6 files changed, 671 insertions(+), 377 deletions(-) rename apps/web/src/query/query-key/__tests__/{use-api-query-key.test.ts => use-service-query-key.test.ts} (60%) delete mode 100644 apps/web/src/query/query-key/use-api-query-key.md delete mode 100644 apps/web/src/query/query-key/use-api-query-key.ts create mode 100644 apps/web/src/query/query-key/use-service-query-key.md create mode 100644 apps/web/src/query/query-key/use-service-query-key.ts diff --git a/apps/web/src/query/query-key/__tests__/use-api-query-key.test.ts b/apps/web/src/query/query-key/__tests__/use-service-query-key.test.ts similarity index 60% rename from apps/web/src/query/query-key/__tests__/use-api-query-key.test.ts rename to apps/web/src/query/query-key/__tests__/use-service-query-key.test.ts index 0c28d4b242..80f6be4536 100644 --- a/apps/web/src/query/query-key/__tests__/use-api-query-key.test.ts +++ b/apps/web/src/query/query-key/__tests__/use-service-query-key.test.ts @@ -1,10 +1,10 @@ -import { ref } from 'vue'; +import { computed } from 'vue'; import { describe, it, expect, vi, } from 'vitest'; -import { _useAPIQueryKey } from '../use-api-query-key'; +import { _useServiceQueryKey } from '../use-service-query-key'; // Mock useQueryKeyAppContext vi.mock('@/query/query-key/_composable/use-app-context-query-key', () => ({ @@ -13,14 +13,14 @@ vi.mock('@/query/query-key/_composable/use-app-context-query-key', () => ({ }), })); -describe('_useAPIQueryKey', () => { +describe('_useServiceQueryKey', () => { it('should generate correct query key structure for basic usage', () => { - const { key } = _useAPIQueryKey( + const { key } = _useServiceQueryKey( 'dashboard', 'public-data-table', 'list', { - params: { page: 1, limit: 10 }, + params: computed(() => ({ page: 1, limit: 10 })), }, ); @@ -39,14 +39,14 @@ describe('_useAPIQueryKey', () => { `); }); - it('should generate correct query key structure with id', () => { - const { key } = _useAPIQueryKey( + it('should generate correct query key structure with contextKey', () => { + const { key } = _useServiceQueryKey( 'dashboard', 'public-data-table', 'get', { - id: 'table-123', - params: { id: 'table-123' }, + contextKey: computed(() => 'table-123'), + params: computed(() => ({ id: 'table-123' })), }, ); @@ -65,48 +65,17 @@ describe('_useAPIQueryKey', () => { `); }); - it('should generate correct query key structure with deps', () => { - const { key } = _useAPIQueryKey( - 'dashboard', - 'public-data-table', - 'list', - { - params: { page: 1, limit: 10 }, - deps: { filter: 'active' }, - }, - ); - - expect(key.value).toMatchInlineSnapshot(` -[ - "workspace", - "workspace-123", - "dashboard", - "public-data-table", - "list", - { - "limit": 10, - "page": 1, - }, - { - "filter": "active", - }, -] -`); - }); - it('should handle reactive values correctly', () => { - const params = ref({ id: 'table-123' }); - const deps = ref({ filter: 'active' }); - const id = ref('table-123'); + const params = computed(() => ({ id: 'table-123' })); + const contextKey = computed(() => 'table-123'); - const { key } = _useAPIQueryKey( + const { key } = _useServiceQueryKey( 'dashboard', 'public-data-table', 'get', { - id, + contextKey, params, - deps, }, ); @@ -121,22 +90,18 @@ describe('_useAPIQueryKey', () => { { "id": "table-123", }, - { - "filter": "active", - }, ] `); }); it('should handle function getters correctly', () => { - const { key } = _useAPIQueryKey( + const { key } = _useServiceQueryKey( 'dashboard', 'public-data-table', 'get', { - id: () => 'table-123', - params: () => ({ id: 'table-123' }), - deps: () => ({ filter: 'active' }), + contextKey: () => 'table-123', + params: computed(() => ({ id: 'table-123' })), }, ); @@ -151,30 +116,26 @@ describe('_useAPIQueryKey', () => { { "id": "table-123", }, - { - "filter": "active", - }, ] `); }); it('should maintain consistent key structure with different option orders', () => { - const params = { id: 'table-123' }; - const deps = { filter: 'active' }; - const id = 'table-123'; + const params = computed(() => ({ id: 'table-123' })); + const contextKey = computed(() => 'table-123'); - const { key: key1 } = _useAPIQueryKey( + const { key: key1 } = _useServiceQueryKey( 'dashboard', 'public-data-table', 'get', - { id, params, deps }, + { contextKey, params }, ); - const { key: key2 } = _useAPIQueryKey( + const { key: key2 } = _useServiceQueryKey( 'dashboard', 'public-data-table', 'get', - { deps, params, id }, + { params, contextKey }, ); expect(key1.value).toEqual(key2.value); @@ -189,9 +150,26 @@ describe('_useAPIQueryKey', () => { { "id": "table-123", }, - { - "filter": "active", - }, +] +`); + }); + + it('should handle withSuffix correctly', () => { + const { withSuffix } = _useServiceQueryKey( + 'dashboard', + 'public-data-table', + 'load', + ); + + const result = withSuffix('table-123'); + expect(result).toMatchInlineSnapshot(` +[ + "workspace", + "workspace-123", + "dashboard", + "public-data-table", + "load", + "table-123", ] `); }); diff --git a/apps/web/src/query/query-key/_helpers/immutable-query-key-helper.ts b/apps/web/src/query/query-key/_helpers/immutable-query-key-helper.ts index 9de930582d..299510c4e6 100644 --- a/apps/web/src/query/query-key/_helpers/immutable-query-key-helper.ts +++ b/apps/web/src/query/query-key/_helpers/immutable-query-key-helper.ts @@ -1,4 +1,4 @@ -export const createImmutableObjectKeyItem = >(obj: T): T => { +export const createImmutableObjectKeyItem = (obj: T): T => { if (obj === null || typeof obj !== 'object') { return obj; } diff --git a/apps/web/src/query/query-key/use-api-query-key.md b/apps/web/src/query/query-key/use-api-query-key.md deleted file mode 100644 index 3467096735..0000000000 --- a/apps/web/src/query/query-key/use-api-query-key.md +++ /dev/null @@ -1,167 +0,0 @@ -# API Query Key Composable - -## Overview -`useAPIQueryKey` is a composable for generating query keys for API requests. It integrates with Tanstack Query's caching system to provide a consistent query key structure. - -## Basic Key Structure -The query key array follows a strict order to ensure consistent caching and invalidation patterns. The order is guaranteed to be: - -1. App Context (workspace/admin context) -2. Service Name -3. Resource Name -4. Verb -5. Resource ID (if applicable) -6. Request Parameters -7. Dependencies (if applicable) - -This predictable structure allows for precise cache management and query invalidation at any level of the hierarchy. - -```typescript -[ - ...appContext, // ['workspace', 'workspaceId'] or ['admin'] - service, // service name (e.g., 'dashboard', 'opsflow') - resource, // resource name (e.g., 'public-data-table') - verb, // action (e.g., 'get', 'list', 'load') - id?, // (optional) resource identifier (with 'get' verb) - params?, // (optional) request parameters - deps? // (optional) dependencies -] -``` - -## Key Options Interface - -```typescript -interface UseAPIQueryKeyOptions

{ - id?: _MaybeRefOrGetter; - params?: _MaybeRefOrGetter

; - deps?: _MaybeRefOrGetter; -} -``` - -## Return Value - -```typescript -interface UseAPIQueryKeyResult

{ - key: ComputedRef; // full query key - namespaces: ComputedRef; // hierarchical structure for cache management - params?: ComputedRef

; // parameters - deps?: ComputedRef; // dependencies (optional) - id?: ComputedRef; // ID (optional) -} -``` - - - -## Usage - -### Basic Usage -```typescript -const { key } = useAPIQueryKey( - 'dashboard', - 'public-data-table', - 'list', - { - params: computed(() => ({ page: 1, limit: 10 })), - } -); -``` - -### With ID -```typescript -const { key } = useAPIQueryKey( - 'dashboard', - 'public-data-table', - 'get', - { - id: toRef(state, 'currentDataTableId'), - params: computed(() => ({ data_table_id: 'table-123' })), - } -); -``` - -### With Dependencies -```typescript -const { key } = _useAPIQueryKey( - 'dashboard', - 'public-data-table', - 'list', - { - params: computed(() => ({ page: 1, limit: 10 }), - deps: computed(() => ({ filter: 'active' }), - } -); -``` - -### With useQuery -```typescript -const { key, params } = _useAPIQueryKey( - 'dashboard', - 'public-data-table', - 'list', - { - params: computed(() => state.params) - } -); - -const { data } = useQuery({ - queryKey: key, - queryFn: () => publicDataTableAPI.list(params.value) -}); -``` - -### Cache Invalidation -```typescript -const { key, namespaces, params } = _useAPIQueryKey( - 'dashboard', - 'public-data-table', - 'get', - { - id: toRef(state, 'currentDataTableId') - params: computed(() => state.params) - deps: computed(() => ({ filter: 'active' }), - } -); - -// Invalidate specific query -queryClient.invalidateQueries({ queryKey: key.value }); - -// Invalidate all queries under a namespace -queryClient.invalidateQueries({ queryKey: [...namespaces.value, state.currentDataTableId] }); -``` - -### Best Practices -1. Always type your params interface for better type safety -2. Use the returned `params` in your `queryFn` to maintain consistency -3. Leverage TypeScript's type inference for better development experience -4. Consider using `deps` for values that should trigger cache invalidation but aren't part of the API request -5. Use `namespaces` for hierarchical cache invalidation when needed - - -## Maintenance Guide - -### Type Definitions -- `ServiceName`: Available service names -- `ResourceName`: Available resource names per service -- `Verb`: Available actions for service and resource - -### Development Environment Validation -- Runtime validation only in development environment -- Validation for required parameters, types, and structure -- Debugging support through warning messages - -### Testing -- Snapshot tests for query key structure validation -- Test cases for various usage scenarios -- Tests for reactive values and function getters - -## Important Notes -1. Query keys must maintain immutability. -2. Object parameters are automatically converted to immutable objects. -3. Runtime validation only runs in development environment. -4. Option object key order does not affect the result. - -## References -- Integrates with Tanstack Query's caching system -- Supports Vue's reactivity system -- Ensures TypeScript type safety -- Provides logging for debugging in development environment diff --git a/apps/web/src/query/query-key/use-api-query-key.ts b/apps/web/src/query/query-key/use-api-query-key.ts deleted file mode 100644 index 971515bcdc..0000000000 --- a/apps/web/src/query/query-key/use-api-query-key.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { toValue } from '@vueuse/core'; -import type { ComputedRef, Ref } from 'vue'; -import { computed } from 'vue'; - -import { useQueryKeyAppContext } from '@/query/query-key/_composable/use-app-context-query-key'; -import { createImmutableObjectKeyItem } from '@/query/query-key/_helpers/immutable-query-key-helper'; -import type { - QueryKeyArray, ResourceName, ServiceName, Verb, -} from '@/query/query-key/_types/query-key-type'; - -// Cache for debug logs -// const debugLogCache = new Map(); -// const DEBUG_LOG_THROTTLE = 1000; // 1초 - - -type _MaybeRefOrGetter = T | Ref | (() => T); -/** - * Options for generating API query keys. - * - * While the options are provided as an object where the order of keys doesn't matter, - * the generated query key will always follow this structure: - * - * ```typescript - * [ - * ...globalContext, - * service, - * resource, - * verb, - * id?, // Optional, appears first in namespace if present - * params?, // Optional, appears second if present - * deps? // Optional, appears last if present - * ] - * - * Note: The order of keys in the options object doesn't affect the final query key structure. - * The query key will always maintain the above order, ensuring predictable cache management. - * - * @property id - Optional identifier for single resource operations (e.g., get, load). - * When present, it appears first in the namespace part of the query key, - * enabling hierarchical cache management for single-resource operations. - * @property params - Optional parameters for the API request. - * @property deps - Optional dependencies that affect the query key. - */ -interface UseAPIQueryKeyOptions { - id?: _MaybeRefOrGetter; - params?: _MaybeRefOrGetter; - deps?: _MaybeRefOrGetter; -} - -type UseAPIQueryKeyResult = { - key: ComputedRef; - namespaces: ComputedRef; - params: T extends undefined ? undefined : ComputedRef; - deps?: ComputedRef; - id?: ComputedRef; -}; - -export const _useAPIQueryKey = , V extends Verb, T extends object = object>( - service: S, - resource: R, - verb: V, - options: UseAPIQueryKeyOptions = {}, -): UseAPIQueryKeyResult => { - // Runtime validation for development environment - if (import.meta.env.DEV) { - if (!service || !resource || !verb) { - console.warn('Required parameters (service, resource, verb) must be provided'); - } - _validateQueryKeyOptions(options); - } - - 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] : []), - ...(resolvedParams ? [createImmutableObjectKeyItem(resolvedParams)] : []), - ...(resolvedDeps ? [createImmutableObjectKeyItem(resolvedDeps)] : []), - ]; - }); - - const namespaces = computed(() => [ - ...globalContext.value, - service, resource, verb, - ]); - - // NOTE: Only for development environment. After using tanstack query devtools, this will be removed. - // if (import.meta.env.DEV) { - // _logQueryKeyDebug(queryKey.value); - // } - - return { - key: queryKey, - namespaces, - params: params ? computed(() => toValue(params)) : undefined, - deps: deps ? computed(() => toValue(deps)) : undefined, - id: id ? computed(() => toValue(id)) : undefined, - } as UseAPIQueryKeyResult; -}; - -const _validateQueryKeyOptions =

(options: { - id?: _MaybeRefOrGetter; - params?: _MaybeRefOrGetter

; - deps?: _MaybeRefOrGetter; -}) => { - if (options.params) { - const rawParams = toValue(options.params); - if (rawParams === null || typeof rawParams !== 'object') { - console.warn('params must be a non-null object'); - } - } - - if (options.deps) { - const rawDeps = toValue(options.deps); - if (rawDeps === null || typeof rawDeps !== 'object') { - console.warn('deps must be a non-null object'); - } - } - - if (options.id) { - const id = toValue(options.id); - if (typeof id !== 'string') { - console.warn('id must be a string'); - } - } -}; - -// const _logQueryKeyDebug = (queryKey: QueryKeyArray) => { -// const now = Date.now(); -// const key = queryKey.join('/'); -// const lastLogTime = debugLogCache.get(key) || 0; - -// if (now - lastLogTime >= DEBUG_LOG_THROTTLE) { -// console.debug('[QueryKey]', { queryKey }); -// debugLogCache.set(key, now); -// } -// }; diff --git a/apps/web/src/query/query-key/use-service-query-key.md b/apps/web/src/query/query-key/use-service-query-key.md new file mode 100644 index 0000000000..6b526372da --- /dev/null +++ b/apps/web/src/query/query-key/use-service-query-key.md @@ -0,0 +1,492 @@ +# Service Query Key Composable +A unified composable for generating structured query keys for TanStack Query in Vue + TypeScript apps. + +## Overview + requests. It integrates with Tanstack Query's caching system to provide a consistent query key structure. + +## Basic Key Structure +The query key array follows a strict order to ensure consistent caching and invalidation patterns. The order is guaranteed to be: + +1. App Context (workspace/admin context) +2. Service Name +3. Resource Name +4. Verb +5. Context Key (if applicable) + - Used to differentiate queries with the same service/resource/verb/params but different UI or logical context + - Example: same API called with different favorite types, themes, or view states +6. Request Parameters + +This predictable structure allows for precise cache management and query invalidation at any level of the hierarchy. + +```typescript +[ + ...appContext, // ['workspace', 'workspaceId'] or ['admin'] + service, // service name (e.g., 'dashboard', 'opsflow') + resource, // resource name (e.g., 'public-data-table') + verb, // action (e.g., 'get', 'list', 'load') + contextKey?, // (optional) contextual data (string, array, or object) + params? // (optional) request parameters +] +``` + +## Key Options Interface + +```typescript +interface UseServiceQueryKeyOptions { + contextKey?: _MaybeRefOrGetter; // string | unknown[] | object + params?: ComputedRef; +} +``` + +## Return Value + +```typescript +interface UseServiceQueryKeyResult { + key: ComputedRef; // full query key + params?: ComputedRef; // parameters + withSuffix: (arg: ContextKeyType) => QueryKeyArray; // dynamic namespace builder +} +``` + +## Query Key Management + +### Importance of Key Structure +The query key structure is carefully designed to ensure consistent cache management. It should be used as-is without modification. + +### Correct Usage Pattern +```typescript +// ✅ Recommended: Using key as-is +const { key } = useServiceQueryKey( + 'dashboard', + 'public-data-table', + 'list', + { + params: computed(() => ({ page: 1, limit: 10 })) + } +); + +const { data } = useQuery({ + queryKey: key, // Use the key directly + queryFn: () => publicDataTableAPI.list(params.value) +}); + +// ❌ Not Recommended: Modifying query key structure +const { data } = useQuery({ + queryKey: [...key.value, 'additional', 'parts'], // Avoid modifying the key structure + queryFn: () => publicDataTableAPI.list(params.value) +}); +``` + +### Why This Matters +1. **Cache Consistency**: Modifying the key structure can lead to cache misses or invalid cache entries +2. **Cache Invalidation**: Proper cache invalidation relies on consistent key structure +3. **Predictability**: The predefined structure ensures predictable cache behavior + +### Best Practices +1. Always use the `key` returned from `useServiceQueryKey` without modification +2. Use `withSuffix` for dynamic namespace creation instead of modifying the key structure +3. Maintain the predefined key structure for consistent cache management + +## Usage + +### Basic Usage +```typescript +const { key } = useServiceQueryKey( + 'dashboard', + 'public-data-table', + 'list', + { + params: computed(() => ({ page: 1, limit: 10 })), + } +); +``` + +### With Context Key +```typescript +const { key } = useServiceQueryKey( + 'dashboard', + 'public-data-table', + 'get', + { + contextKey: computed(() => state.dataTableId), + params: computed(() => ({ page: 1, limit: 10 })), + } +); +``` + +### With useQuery +```typescript +const { key, params } = useServiceQueryKey( + 'dashboard', + 'public-data-table', + 'list', + { + params: computed(() => state.params) + } +); + +const { data } = useQuery({ + queryKey: key, + queryFn: () => publicDataTableAPI.list(params.value) +}); +``` + +## Query Parameters Management + +### Importance of params +The `params` returned from `useServiceQueryKey` is crucial for maintaining consistent cache management. It should always be used as the parameters for the `queryFn` to ensure proper cache key generation and invalidation. + +### Correct Usage Pattern +```typescript +// ✅ Recommended: Using params from useServiceQueryKey +const { key, params } = useServiceQueryKey( + 'dashboard', + 'public-data-table', + 'list', + { + params: computed(() => ({ page: 1, limit: 10 })) + } +); + +// ✅ Use params from useServiceQueryKey +const { data } = useQuery({ + queryKey: key, + queryFn: () => publicDataTableAPI.list(params.value) // Don't forget to use `.value` if params is reactive +}); + +// ❌ Not Recommended: Creating params separately +const { data } = useQuery({ + queryKey: key, + queryFn: () => publicDataTableAPI.list({ page: 1, limit: 10 }) // Avoid creating params separately +}); +``` + +### Why This Matters +1. **Cache Consistency**: Using the same params object ensures that the cache key matches the actual API request parameters +2. **Cache Invalidation**: Proper cache invalidation relies on consistent parameter handling +3. **Type Safety**: The params object maintains type safety throughout the query lifecycle + +### Best Practices +1. Always use the `params` returned from `useServiceQueryKey` in your `queryFn` +2. Keep params reactive using `computed` +3. Maintain type safety by properly typing your params + +## Dynamic Namespace with withSuffix + +`withSuffix` is a method designed for imperative cache invalidation scenarios, particularly useful when you need to create dynamic query key namespaces that weren't available at the declaration time. + +### Key vs withSuffix + +| Purpose | Method | Example | When to Use | +|--------------------------|--------------|--------------------------------------------------------|------------------------------------------------| +| Declarative data fetch | `key` | `useQuery({ queryKey: key })` | During component setup or reactive queries | +| Contextual invalidation | `withSuffix` | `queryClient.invalidateQueries({ queryKey: withSuffix(id) })` | When additional runtime context is needed | + +- `key` is used for declarative data fetching and most standard cache invalidations. +- `withSuffix` should be used **only when additional dynamic context (like an ID or variant) is needed** at runtime for imperative cache control. +- It is not required for every cache invalidation — use it selectively. +- Avoid modifying the `key` manually; prefer `withSuffix` when runtime extensions are necessary. + +### Use Cases +1. Cache invalidation in mutation callbacks +2. Dynamic namespace creation in imperative code +3. Context-specific cache management + +### Example +```typescript +// In use-data-table-cascade-update.ts +const { withSuffix } = useServiceQueryKey( + 'dashboard', + 'public-data-table', + 'load' +); + +// Dynamic cache invalidation based on data table type +const _invalidateLoadQueries = async (data: DataTableModel) => { + await queryClient.invalidateQueries({ + queryKey: withSuffix(data.data_table_id), // ['admin', 'dashboard', 'public-dashboard', 'load', 'dt-123'] + }); +}; +``` + +### Benefits +1. **Imperative Cache Control**: Aligns with Tanstack Query's imperative invalidation philosophy +2. **Dynamic Context**: Allows creation of context-specific namespaces at runtime +3. **Type Safety**: Maintains type safety while providing flexibility +4. **Performance**: Includes built-in caching for object-based suffixes + +### Best Practices +1. Use `withSuffix` primarily for cache invalidation scenarios +2. Leverage object caching for frequently used suffixes +3. Consider the performance implications of suffix caching +4. Use type-safe context keys when possible + +## Maintenance Guide + +### Type Definitions +- `ServiceName`: Available service names +- `ResourceName`: Available resource names per service +- `Verb`: Available actions for service and resource +- `ContextKeyType`: string | unknown[] | object + +### Development Environment Validation +- Runtime validation only in development environment +- Validation for required parameters and types +- Debugging support through warning messages + +## Important Notes +1. Query keys must maintain immutability +2. Object parameters are automatically converted to immutable objects +3. Runtime validation only runs in development environment +4. Option object key order does not affect the result +5. `withSuffix` results are cached for object-based suffixes + +## References +- Integrates with Tanstack Query's caching system +- Supports Vue's reactivity system +- Ensures TypeScript type safety +- Provides logging for debugging in development environment + requests. It integrates with Tanstack Query's caching system to provide a consistent query key structure. + +## Basic Key Structure +The query key array follows a strict order to ensure consistent caching and invalidation patterns. The order is guaranteed to be: + +1. App Context (workspace/admin context) +2. Service Name +3. Resource Name +4. Verb +5. Context Key (if applicable) + - Used to differentiate queries with the same service/resource/verb/params but different UI or logical context + - Example: same API called with different favorite types, themes, or view states +6. Request Parameters + +This predictable structure allows for precise cache management and query invalidation at any level of the hierarchy. + +```typescript +[ + ...appContext, // ['workspace', 'workspaceId'] or ['admin'] + service, // service name (e.g., 'dashboard', 'opsflow') + resource, // resource name (e.g., 'public-data-table') + verb, // action (e.g., 'get', 'list', 'load') + contextKey?, // (optional) contextual data (string, array, or object) + params? // (optional) request parameters +] +``` + +## Key Options Interface + +```typescript +interface UseServiceQueryKeyOptions { + contextKey?: _MaybeRefOrGetter; // string | unknown[] | object + params?: ComputedRef; +} +``` + +## Return Value + +```typescript +interface UseServiceQueryKeyResult { + key: ComputedRef; // full query key + params?: ComputedRef; // parameters + withSuffix: (arg: ContextKeyType) => QueryKeyArray; // dynamic namespace builder +} +``` + +## Query Key Management + +### Importance of Key Structure +The query key structure is carefully designed to ensure consistent cache management. It should be used as-is without modification. + +### Correct Usage Pattern +```typescript +// ✅ Recommended: Using key as-is +const { key } = useServiceQueryKey( + 'dashboard', + 'public-data-table', + 'list', + { + params: computed(() => ({ page: 1, limit: 10 })) + } +); + +const { data } = useQuery({ + queryKey: key, // Use the key directly + queryFn: () => publicDataTableAPI.list(params.value) +}); + +// ❌ Not Recommended: Modifying query key structure +const { data } = useQuery({ + queryKey: [...key.value, 'additional', 'parts'], // Avoid modifying the key structure + queryFn: () => publicDataTableAPI.list(params.value) +}); +``` + +### Why This Matters +1. **Cache Consistency**: Modifying the key structure can lead to cache misses or invalid cache entries +2. **Cache Invalidation**: Proper cache invalidation relies on consistent key structure +3. **Predictability**: The predefined structure ensures predictable cache behavior + +### Best Practices +1. Always use the `key` returned from `useServiceQueryKey` without modification +2. Use `withSuffix` for dynamic namespace creation instead of modifying the key structure +3. Maintain the predefined key structure for consistent cache management + +## Usage + +### Basic Usage +```typescript +const { key } = useServiceQueryKey( + 'dashboard', + 'public-data-table', + 'list', + { + params: computed(() => ({ page: 1, limit: 10 })), + } +); +``` + +### With Context Key +```typescript +const { key } = useServiceQueryKey( + 'dashboard', + 'public-data-table', + 'get', + { + contextKey: computed(() => state.dataTableId), + params: computed(() => ({ page: 1, limit: 10 })), + } +); +``` + +### With useQuery +```typescript +const { key, params } = useServiceQueryKey( + 'dashboard', + 'public-data-table', + 'list', + { + params: computed(() => state.params) + } +); + +const { data } = useQuery({ + queryKey: key, + queryFn: () => publicDataTableAPI.list(params.value) +}); +``` + +## Query Parameters Management + +### Importance of params +The `params` returned from `useServiceQueryKey` is crucial for maintaining consistent cache management. It should always be used as the parameters for the `queryFn` to ensure proper cache key generation and invalidation. + +### Correct Usage Pattern +```typescript +// ✅ Recommended: Using params from useServiceQueryKey +const { key, params } = useServiceQueryKey( + 'dashboard', + 'public-data-table', + 'list', + { + params: computed(() => ({ page: 1, limit: 10 })) + } +); + +// ✅ Use params from useServiceQueryKey +const { data } = useQuery({ + queryKey: key, + queryFn: () => publicDataTableAPI.list(params.value) // Don't forget to use `.value` if params is reactive +}); + +// ❌ Not Recommended: Creating params separately +const { data } = useQuery({ + queryKey: key, + queryFn: () => publicDataTableAPI.list({ page: 1, limit: 10 }) // Avoid creating params separately +}); +``` + +### Why This Matters +1. **Cache Consistency**: Using the same params object ensures that the cache key matches the actual API request parameters +2. **Cache Invalidation**: Proper cache invalidation relies on consistent parameter handling +3. **Type Safety**: The params object maintains type safety throughout the query lifecycle + +### Best Practices +1. Always use the `params` returned from `useServiceQueryKey` in your `queryFn` +2. Keep params reactive using `computed` +3. Maintain type safety by properly typing your params + +## Dynamic Namespace with withSuffix + +`withSuffix` is a method designed for imperative cache invalidation scenarios, particularly useful when you need to create dynamic query key namespaces that weren't available at the declaration time. + +### Key vs withSuffix + +| Purpose | Method | Example | When to Use | +|--------------------------|--------------|--------------------------------------------------------|------------------------------------------------| +| Declarative data fetch | `key` | `useQuery({ queryKey: key })` | During component setup or reactive queries | +| Contextual invalidation | `withSuffix` | `queryClient.invalidateQueries({ queryKey: withSuffix(id) })` | When additional runtime context is needed | + +- `key` is used for declarative data fetching and most standard cache invalidations. +- `withSuffix` should be used **only when additional dynamic context (like an ID or variant) is needed** at runtime for imperative cache control. +- It is not required for every cache invalidation — use it selectively. +- Avoid modifying the `key` manually; prefer `withSuffix` when runtime extensions are necessary. + +### Use Cases +1. Cache invalidation in mutation callbacks +2. Dynamic namespace creation in imperative code +3. Context-specific cache management + +### Example +```typescript +// In use-data-table-cascade-update.ts +const { withSuffix } = useServiceQueryKey( + 'dashboard', + 'public-data-table', + 'load' +); + +// Dynamic cache invalidation based on data table type +const _invalidateLoadQueries = async (data: DataTableModel) => { + await queryClient.invalidateQueries({ + queryKey: withSuffix(data.data_table_id), // ['admin', 'dashboard', 'public-dashboard', 'load', 'dt-123'] + }); +}; +``` + +### Benefits +1. **Imperative Cache Control**: Aligns with Tanstack Query's imperative invalidation philosophy +2. **Dynamic Context**: Allows creation of context-specific namespaces at runtime +3. **Type Safety**: Maintains type safety while providing flexibility +4. **Performance**: Includes built-in caching for object-based suffixes + +### Best Practices +1. Use `withSuffix` primarily for cache invalidation scenarios +2. Leverage object caching for frequently used suffixes +3. Consider the performance implications of suffix caching +4. Use type-safe context keys when possible + +## Maintenance Guide + +### Type Definitions +- `ServiceName`: Available service names +- `ResourceName`: Available resource names per service +- `Verb`: Available actions for service and resource +- `ContextKeyType`: string | unknown[] | object + +### Development Environment Validation +- Runtime validation only in development environment +- Validation for required parameters and types +- Debugging support through warning messages + +## Important Notes +1. Query keys must maintain immutability +2. Object parameters are automatically converted to immutable objects +3. Runtime validation only runs in development environment +4. Option object key order does not affect the result +5. `withSuffix` results are cached for object-based suffixes + +## References +- Integrates with Tanstack Query's caching system +- Supports Vue's reactivity system +- Ensures TypeScript type safety +- Provides logging for debugging in development environment diff --git a/apps/web/src/query/query-key/use-service-query-key.ts b/apps/web/src/query/query-key/use-service-query-key.ts new file mode 100644 index 0000000000..079ac73f92 --- /dev/null +++ b/apps/web/src/query/query-key/use-service-query-key.ts @@ -0,0 +1,136 @@ +import { toValue } from '@vueuse/core'; +import type { Ref, ComputedRef } from 'vue'; +import { computed } from 'vue'; + +import { useQueryKeyAppContext } from '@/query/query-key/_composable/use-app-context-query-key'; +import { createImmutableObjectKeyItem } from '@/query/query-key/_helpers/immutable-query-key-helper'; +import type { + QueryKeyArray, ResourceName, ServiceName, Verb, +} from '@/query/query-key/_types/query-key-type'; + +// Cache for debug logs +// const debugLogCache = new Map(); +// const DEBUG_LOG_THROTTLE = 1000; // 1초 + + +type _MaybeRefOrGetter = T | Ref | (() => T); +/** + * Options for generating service query keys. + * + * While the options are provided as an object where the order of keys doesn't matter, + * the generated query key will always follow this structure: + * + * ```typescript + * [ + * ...globalContext, + * service, + * resource, + * verb, + * contextKey?, // Optional, appears first in namespace if present + * params?, // Optional, appears second if present + * ] + * + * Note: The order of keys in the options object doesn't affect the final query key structure. + * The query key will always maintain the above order, ensuring predictable cache management. + * + * @property contextKey - Optional key for contextual data (string, array, or object). + * When present, it appears first in the namespace part of the query key, + * enabling contextual cache management. + * @property params - Optional parameters for the API request. + * When present, it appears second in the namespace part of the query key. + */ +interface UseServiceQueryKeyOptions { + contextKey?: _MaybeRefOrGetter; + params?: ComputedRef; +} +type ContextKeyType = string|unknown[]|object; + +type UseServiceQueryKeyResult = { + key: ComputedRef; + params: T extends undefined ? undefined : ComputedRef; + withSuffix: (arg: ContextKeyType) => QueryKeyArray; +}; + +export const _useServiceQueryKey = , V extends Verb, T extends object = object>( + service: S, + resource: R, + verb: V, + options: UseServiceQueryKeyOptions = {}, +): UseServiceQueryKeyResult => { + // Runtime validation for development environment + if (import.meta.env.DEV) { + if (!service || !resource || !verb) { + console.warn('Required parameters (service, resource, verb) must be provided'); + } + if (options.params) { + const rawParams = toValue(options.params); + if (rawParams === null || typeof rawParams !== 'object') { + console.warn('params must be a non-null object'); + } + } + } + + const { params, contextKey } = options; + const queryKeyAppContext = useQueryKeyAppContext(); + + + const memoizedContextKey = computed(() => { + const resolvedContextKey = toValue(contextKey); + return resolvedContextKey + ? _normalizeQueryKeyPart(createImmutableObjectKeyItem(resolvedContextKey)) + : []; + }); + + const queryKey = computed(() => { + const resolvedParams = toValue(params); + + return [ + ...queryKeyAppContext.value, + service, resource, verb, + ...memoizedContextKey.value, + ...(resolvedParams ? [createImmutableObjectKeyItem(resolvedParams)] : []), + ]; + }); + + // NOTE: Only for development environment. After using tanstack query devtools, this will be removed. + // if (import.meta.env.DEV) { + // _logQueryKeyDebug(queryKey.value); + // } + + + const suffixCache = new WeakMap(); + return { + key: queryKey, + params, + withSuffix: (arg) => { + if (typeof arg === 'object' && arg !== null) { + const cached = suffixCache.get(arg); + if (cached) return cached; + + const result = [...queryKey.value, ..._normalizeQueryKeyPart(createImmutableObjectKeyItem(arg))]; + suffixCache.set(arg, result); + return result; + } + return [...queryKey.value, arg]; + }, + } as UseServiceQueryKeyResult; +}; + + +const _normalizeQueryKeyPart = (key: unknown): QueryKeyArray => { + if (Array.isArray(key)) { + return key; + } + return [key]; +}; + +// const _logQueryKeyDebug = (queryKey: QueryKeyArray) => { +// const now = Date.now(); +// const key = queryKey.join('/'); +// const lastLogTime = debugLogCache.get(key) || 0; + +// if (now - lastLogTime >= DEBUG_LOG_THROTTLE) { +// console.debug('[QueryKey]', { queryKey }); +// debugLogCache.set(key, now); +// } +// }; From 9fbb27bf82e58bf82b0c59d4025ba14b7ec7f086 Mon Sep 17 00:00:00 2001 From: Yongtae Park Date: Thu, 27 Mar 2025 14:41:46 +0900 Subject: [PATCH 12/17] chore(global-query-client): add annotation Signed-off-by: samuel.park --- apps/web/src/main.ts | 4 ++-- apps/web/src/query/clients.ts | 12 +++++++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/apps/web/src/main.ts b/apps/web/src/main.ts index 768eb7729a..9d3377b8f2 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 { apiQueryClient } from '@/query/clients'; +import { serviceQueryClient } from '@/query/clients'; import { SpaceRouter } from '@/router'; import { i18n } from '@/translations'; @@ -30,7 +30,7 @@ Vue.use(Fragment.Plugin); Vue.use(VTooltip, { defaultClass: 'p-tooltip', defaultBoundariesElement: document.body }); Vue.use(PortalVue); Vue.use(PiniaVuePlugin); -Vue.use(VueQueryPlugin, { defaultQueryClient: apiQueryClient }); +Vue.use(VueQueryPlugin, { defaultQueryClient: serviceQueryClient }); directive(Vue); diff --git a/apps/web/src/query/clients.ts b/apps/web/src/query/clients.ts index b12aa51602..3efb0ec11d 100644 --- a/apps/web/src/query/clients.ts +++ b/apps/web/src/query/clients.ts @@ -1,5 +1,15 @@ import { QueryClient } from '@tanstack/vue-query'; -export const apiQueryClient = new QueryClient(); +/** + * Main query client for service-level data fetching and caching. + * This is the default query client used throughout the application. + * It's automatically injected when useQueryClient() is called in service context. + */ +export const serviceQueryClient = new QueryClient(); +/** + * Dedicated query client for the reference data system. + * This client is used internally by the reference data system for managing its own cache. + * It's not meant to be used directly in service context. + */ export const referenceQueryClient = new QueryClient(); From 5ea61e6b86643ba2ed721d98e6a0924fa9d266f2 Mon Sep 17 00:00:00 2001 From: Yongtae Park Date: Thu, 27 Mar 2025 14:46:19 +0900 Subject: [PATCH 13/17] chore: solve conflict Signed-off-by: samuel.park --- .../query/query-key/use-service-query-key.md | 246 +----------------- 1 file changed, 1 insertion(+), 245 deletions(-) diff --git a/apps/web/src/query/query-key/use-service-query-key.md b/apps/web/src/query/query-key/use-service-query-key.md index 6b526372da..600207f23d 100644 --- a/apps/web/src/query/query-key/use-service-query-key.md +++ b/apps/web/src/query/query-key/use-service-query-key.md @@ -2,251 +2,7 @@ A unified composable for generating structured query keys for TanStack Query in Vue + TypeScript apps. ## Overview - requests. It integrates with Tanstack Query's caching system to provide a consistent query key structure. - -## Basic Key Structure -The query key array follows a strict order to ensure consistent caching and invalidation patterns. The order is guaranteed to be: - -1. App Context (workspace/admin context) -2. Service Name -3. Resource Name -4. Verb -5. Context Key (if applicable) - - Used to differentiate queries with the same service/resource/verb/params but different UI or logical context - - Example: same API called with different favorite types, themes, or view states -6. Request Parameters - -This predictable structure allows for precise cache management and query invalidation at any level of the hierarchy. - -```typescript -[ - ...appContext, // ['workspace', 'workspaceId'] or ['admin'] - service, // service name (e.g., 'dashboard', 'opsflow') - resource, // resource name (e.g., 'public-data-table') - verb, // action (e.g., 'get', 'list', 'load') - contextKey?, // (optional) contextual data (string, array, or object) - params? // (optional) request parameters -] -``` - -## Key Options Interface - -```typescript -interface UseServiceQueryKeyOptions { - contextKey?: _MaybeRefOrGetter; // string | unknown[] | object - params?: ComputedRef; -} -``` - -## Return Value - -```typescript -interface UseServiceQueryKeyResult { - key: ComputedRef; // full query key - params?: ComputedRef; // parameters - withSuffix: (arg: ContextKeyType) => QueryKeyArray; // dynamic namespace builder -} -``` - -## Query Key Management - -### Importance of Key Structure -The query key structure is carefully designed to ensure consistent cache management. It should be used as-is without modification. - -### Correct Usage Pattern -```typescript -// ✅ Recommended: Using key as-is -const { key } = useServiceQueryKey( - 'dashboard', - 'public-data-table', - 'list', - { - params: computed(() => ({ page: 1, limit: 10 })) - } -); - -const { data } = useQuery({ - queryKey: key, // Use the key directly - queryFn: () => publicDataTableAPI.list(params.value) -}); - -// ❌ Not Recommended: Modifying query key structure -const { data } = useQuery({ - queryKey: [...key.value, 'additional', 'parts'], // Avoid modifying the key structure - queryFn: () => publicDataTableAPI.list(params.value) -}); -``` - -### Why This Matters -1. **Cache Consistency**: Modifying the key structure can lead to cache misses or invalid cache entries -2. **Cache Invalidation**: Proper cache invalidation relies on consistent key structure -3. **Predictability**: The predefined structure ensures predictable cache behavior - -### Best Practices -1. Always use the `key` returned from `useServiceQueryKey` without modification -2. Use `withSuffix` for dynamic namespace creation instead of modifying the key structure -3. Maintain the predefined key structure for consistent cache management - -## Usage - -### Basic Usage -```typescript -const { key } = useServiceQueryKey( - 'dashboard', - 'public-data-table', - 'list', - { - params: computed(() => ({ page: 1, limit: 10 })), - } -); -``` - -### With Context Key -```typescript -const { key } = useServiceQueryKey( - 'dashboard', - 'public-data-table', - 'get', - { - contextKey: computed(() => state.dataTableId), - params: computed(() => ({ page: 1, limit: 10 })), - } -); -``` - -### With useQuery -```typescript -const { key, params } = useServiceQueryKey( - 'dashboard', - 'public-data-table', - 'list', - { - params: computed(() => state.params) - } -); - -const { data } = useQuery({ - queryKey: key, - queryFn: () => publicDataTableAPI.list(params.value) -}); -``` - -## Query Parameters Management - -### Importance of params -The `params` returned from `useServiceQueryKey` is crucial for maintaining consistent cache management. It should always be used as the parameters for the `queryFn` to ensure proper cache key generation and invalidation. - -### Correct Usage Pattern -```typescript -// ✅ Recommended: Using params from useServiceQueryKey -const { key, params } = useServiceQueryKey( - 'dashboard', - 'public-data-table', - 'list', - { - params: computed(() => ({ page: 1, limit: 10 })) - } -); - -// ✅ Use params from useServiceQueryKey -const { data } = useQuery({ - queryKey: key, - queryFn: () => publicDataTableAPI.list(params.value) // Don't forget to use `.value` if params is reactive -}); - -// ❌ Not Recommended: Creating params separately -const { data } = useQuery({ - queryKey: key, - queryFn: () => publicDataTableAPI.list({ page: 1, limit: 10 }) // Avoid creating params separately -}); -``` - -### Why This Matters -1. **Cache Consistency**: Using the same params object ensures that the cache key matches the actual API request parameters -2. **Cache Invalidation**: Proper cache invalidation relies on consistent parameter handling -3. **Type Safety**: The params object maintains type safety throughout the query lifecycle - -### Best Practices -1. Always use the `params` returned from `useServiceQueryKey` in your `queryFn` -2. Keep params reactive using `computed` -3. Maintain type safety by properly typing your params - -## Dynamic Namespace with withSuffix - -`withSuffix` is a method designed for imperative cache invalidation scenarios, particularly useful when you need to create dynamic query key namespaces that weren't available at the declaration time. - -### Key vs withSuffix - -| Purpose | Method | Example | When to Use | -|--------------------------|--------------|--------------------------------------------------------|------------------------------------------------| -| Declarative data fetch | `key` | `useQuery({ queryKey: key })` | During component setup or reactive queries | -| Contextual invalidation | `withSuffix` | `queryClient.invalidateQueries({ queryKey: withSuffix(id) })` | When additional runtime context is needed | - -- `key` is used for declarative data fetching and most standard cache invalidations. -- `withSuffix` should be used **only when additional dynamic context (like an ID or variant) is needed** at runtime for imperative cache control. -- It is not required for every cache invalidation — use it selectively. -- Avoid modifying the `key` manually; prefer `withSuffix` when runtime extensions are necessary. - -### Use Cases -1. Cache invalidation in mutation callbacks -2. Dynamic namespace creation in imperative code -3. Context-specific cache management - -### Example -```typescript -// In use-data-table-cascade-update.ts -const { withSuffix } = useServiceQueryKey( - 'dashboard', - 'public-data-table', - 'load' -); - -// Dynamic cache invalidation based on data table type -const _invalidateLoadQueries = async (data: DataTableModel) => { - await queryClient.invalidateQueries({ - queryKey: withSuffix(data.data_table_id), // ['admin', 'dashboard', 'public-dashboard', 'load', 'dt-123'] - }); -}; -``` - -### Benefits -1. **Imperative Cache Control**: Aligns with Tanstack Query's imperative invalidation philosophy -2. **Dynamic Context**: Allows creation of context-specific namespaces at runtime -3. **Type Safety**: Maintains type safety while providing flexibility -4. **Performance**: Includes built-in caching for object-based suffixes - -### Best Practices -1. Use `withSuffix` primarily for cache invalidation scenarios -2. Leverage object caching for frequently used suffixes -3. Consider the performance implications of suffix caching -4. Use type-safe context keys when possible - -## Maintenance Guide - -### Type Definitions -- `ServiceName`: Available service names -- `ResourceName`: Available resource names per service -- `Verb`: Available actions for service and resource -- `ContextKeyType`: string | unknown[] | object - -### Development Environment Validation -- Runtime validation only in development environment -- Validation for required parameters and types -- Debugging support through warning messages - -## Important Notes -1. Query keys must maintain immutability -2. Object parameters are automatically converted to immutable objects -3. Runtime validation only runs in development environment -4. Option object key order does not affect the result -5. `withSuffix` results are cached for object-based suffixes - -## References -- Integrates with Tanstack Query's caching system -- Supports Vue's reactivity system -- Ensures TypeScript type safety -- Provides logging for debugging in development environment - requests. It integrates with Tanstack Query's caching system to provide a consistent query key structure. +`useServiceQueryKey` is a composable for generating query keys for API requests. It integrates with Tanstack Query's caching system to provide a consistent query key structure. ## Basic Key Structure The query key array follows a strict order to ensure consistent caching and invalidation patterns. The order is guaranteed to be: From 226546e76eda556df0b7017dc770961200762bad Mon Sep 17 00:00:00 2001 From: Yongtae Park Date: Thu, 27 Mar 2025 14:52:03 +0900 Subject: [PATCH 14/17] chore: small fix Signed-off-by: samuel.park --- apps/web/src/query/query-key/use-service-query-key.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/apps/web/src/query/query-key/use-service-query-key.md b/apps/web/src/query/query-key/use-service-query-key.md index 600207f23d..5e27e66c15 100644 --- a/apps/web/src/query/query-key/use-service-query-key.md +++ b/apps/web/src/query/query-key/use-service-query-key.md @@ -99,6 +99,8 @@ const { key } = useServiceQueryKey( params: computed(() => ({ page: 1, limit: 10 })), } ); + +// Key Result : ['workspace', 'workspace-123', 'dashboard', 'public-data-table', 'list', { page: 1, limit: 10 }] ``` ### With Context Key @@ -112,6 +114,8 @@ const { key } = useServiceQueryKey( params: computed(() => ({ page: 1, limit: 10 })), } ); + +// Key Result : ['workspace', 'workspace-123', 'dashboard', 'public-data-table', 'get', 'table-123', { page: 1, limit: 10 }] ``` ### With useQuery @@ -129,6 +133,9 @@ const { data } = useQuery({ queryKey: key, queryFn: () => publicDataTableAPI.list(params.value) }); + +// Key Result : ['workspace', 'workspace-123', 'dashboard', 'public-data-table', 'list', { page: 1, limit: 10 }] +// Params Result : { page: 1, limit: 10 } (ComputedRef Value) ``` ## Query Parameters Management From 1e01cf8444fad3cdcf56282dd86a3e5cf9b8fa01 Mon Sep 17 00:00:00 2001 From: Yongtae Park Date: Thu, 27 Mar 2025 15:57:29 +0900 Subject: [PATCH 15/17] fix(service-query-key): apply opened type to params Signed-off-by: samuel.park --- apps/web/src/query/query-key/use-service-query-key.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/web/src/query/query-key/use-service-query-key.ts b/apps/web/src/query/query-key/use-service-query-key.ts index 079ac73f92..90e8fcbce7 100644 --- a/apps/web/src/query/query-key/use-service-query-key.ts +++ b/apps/web/src/query/query-key/use-service-query-key.ts @@ -13,7 +13,7 @@ import type { // const DEBUG_LOG_THROTTLE = 1000; // 1초 -type _MaybeRefOrGetter = T | Ref | (() => T); +type _MaybeRefOrGetter = T | Ref | ComputedRef | (() => T); /** * Options for generating service query keys. * @@ -41,13 +41,13 @@ type _MaybeRefOrGetter = T | Ref | (() => T); */ interface UseServiceQueryKeyOptions { contextKey?: _MaybeRefOrGetter; - params?: ComputedRef; + params?: _MaybeRefOrGetter; } type ContextKeyType = string|unknown[]|object; type UseServiceQueryKeyResult = { key: ComputedRef; - params: T extends undefined ? undefined : ComputedRef; + params: T extends undefined ? undefined : Readonly; withSuffix: (arg: ContextKeyType) => QueryKeyArray; }; @@ -101,7 +101,9 @@ export const _useServiceQueryKey = (); return { key: queryKey, - params, + params: params + ? Object.freeze(toValue(params)) as Readonly + : undefined, withSuffix: (arg) => { if (typeof arg === 'object' && arg !== null) { const cached = suffixCache.get(arg); From 46485e5345f3134478bd79fc68be62148d129221 Mon Sep 17 00:00:00 2001 From: Yongtae Park Date: Thu, 27 Mar 2025 16:19:58 +0900 Subject: [PATCH 16/17] chore(docs&test): apply changed return type Signed-off-by: samuel.park --- .../__tests__/use-service-query-key.test.ts | 2 +- .../src/query/query-key/use-service-query-key.md | 15 +++++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/apps/web/src/query/query-key/__tests__/use-service-query-key.test.ts b/apps/web/src/query/query-key/__tests__/use-service-query-key.test.ts index 80f6be4536..a610828907 100644 --- a/apps/web/src/query/query-key/__tests__/use-service-query-key.test.ts +++ b/apps/web/src/query/query-key/__tests__/use-service-query-key.test.ts @@ -20,7 +20,7 @@ describe('_useServiceQueryKey', () => { 'public-data-table', 'list', { - params: computed(() => ({ page: 1, limit: 10 })), + params: { page: 1, limit: 10 }, }, ); diff --git a/apps/web/src/query/query-key/use-service-query-key.md b/apps/web/src/query/query-key/use-service-query-key.md index 5e27e66c15..4714b1e55e 100644 --- a/apps/web/src/query/query-key/use-service-query-key.md +++ b/apps/web/src/query/query-key/use-service-query-key.md @@ -43,7 +43,7 @@ interface UseServiceQueryKeyOptions { ```typescript interface UseServiceQueryKeyResult { key: ComputedRef; // full query key - params?: ComputedRef; // parameters + params?: Readonly; // parameters withSuffix: (arg: ContextKeyType) => QueryKeyArray; // dynamic namespace builder } ``` @@ -56,7 +56,7 @@ The query key structure is carefully designed to ensure consistent cache managem ### Correct Usage Pattern ```typescript // ✅ Recommended: Using key as-is -const { key } = useServiceQueryKey( +const { key, params } = useServiceQueryKey( 'dashboard', 'public-data-table', 'list', @@ -67,13 +67,13 @@ const { key } = useServiceQueryKey( const { data } = useQuery({ queryKey: key, // Use the key directly - queryFn: () => publicDataTableAPI.list(params.value) + queryFn: () => publicDataTableAPI.list(params) }); // ❌ Not Recommended: Modifying query key structure const { data } = useQuery({ queryKey: [...key.value, 'additional', 'parts'], // Avoid modifying the key structure - queryFn: () => publicDataTableAPI.list(params.value) + queryFn: () => publicDataTableAPI.list(params) }); ``` @@ -131,7 +131,7 @@ const { key, params } = useServiceQueryKey( const { data } = useQuery({ queryKey: key, - queryFn: () => publicDataTableAPI.list(params.value) + queryFn: () => publicDataTableAPI.list(params) }); // Key Result : ['workspace', 'workspace-123', 'dashboard', 'public-data-table', 'list', { page: 1, limit: 10 }] @@ -158,7 +158,7 @@ const { key, params } = useServiceQueryKey( // ✅ Use params from useServiceQueryKey const { data } = useQuery({ queryKey: key, - queryFn: () => publicDataTableAPI.list(params.value) // Don't forget to use `.value` if params is reactive + queryFn: () => publicDataTableAPI.list(params) }); // ❌ Not Recommended: Creating params separately @@ -175,8 +175,7 @@ const { data } = useQuery({ ### Best Practices 1. Always use the `params` returned from `useServiceQueryKey` in your `queryFn` -2. Keep params reactive using `computed` -3. Maintain type safety by properly typing your params +2. Maintain type safety by properly typing your params ## Dynamic Namespace with withSuffix From 48f6134b3773ce953b4eb67195db0e06314dcba8 Mon Sep 17 00:00:00 2001 From: Yongtae Park Date: Fri, 28 Mar 2025 16:18:31 +0900 Subject: [PATCH 17/17] chore: remove unnecessary reactive Signed-off-by: samuel.park --- .../_composable/use-app-context-query-key.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/apps/web/src/query/query-key/_composable/use-app-context-query-key.ts b/apps/web/src/query/query-key/_composable/use-app-context-query-key.ts index 0077545a53..2192def019 100644 --- a/apps/web/src/query/query-key/_composable/use-app-context-query-key.ts +++ b/apps/web/src/query/query-key/_composable/use-app-context-query-key.ts @@ -1,4 +1,4 @@ -import { computed, reactive } from 'vue'; +import { computed } from 'vue'; import { useAppContextStore } from '@/store/app-context/app-context-store'; import { useUserWorkspaceStore } from '@/store/app-context/workspace/user-workspace-store'; @@ -23,18 +23,16 @@ export const useQueryKeyAppContext = () => { const appContextStore = useAppContextStore(); const userWorkspaceStore = useUserWorkspaceStore(); - const _state = reactive({ - isAdminMode: computed(() => appContextStore.getters.isAdminMode), - workspaceId: computed(() => userWorkspaceStore.getters.currentWorkspaceId), - }); + const _isAdminMode = computed(() => appContextStore.getters.isAdminMode); + const _workspaceId = computed(() => userWorkspaceStore.getters.currentWorkspaceId); return computed(() => { - const state: QueryKeyState = _state.isAdminMode + const state: QueryKeyState = _isAdminMode.value ? { isAdminMode: true } : { isAdminMode: false, // workspaceId: _state.workspaceId! - workspaceId: _state.workspaceId ?? '', + workspaceId: _workspaceId.value ?? '', }; return state.isAdminMode