Skip to content

feat(scoped-query): introduce useScopedQuery for scope-based API access control #5754

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Apr 14, 2025

Conversation

piggggggggy
Copy link
Member

Skip Review (optional)

  • Minor changes that don't affect the functionality (e.g. style, chore, ci, test, docs)
  • Previously reviewed in feature branch, further review is not mandatory
  • Self-merge allowed for solo developers or urgent changes

Description (optional)

Summary

This PR introduces a new composable useScopedQuery, a wrapper around useQuery that enforces scope-based API execution logic for our product.

Why this is needed

Our product uses API-level scopes to control access permissions. However, these scopes are only loosely coupled with route-level contexts.
Without explicit enforcement, queries may execute before the correct scope is granted, leading to authorization errors or broken UX.

To solve this, useScopedQuery ensures:

  • Queries only run when the user’s grant scope is valid.
  • Global loading states are respected.
  • Reactive enabled values are supported and merged with scope logic.
  • Warning logs are shown in development if the query is blocked due to scope.

Features

  • Compatible with all standard useQuery options
  • Supports reactive or static enabled
  • Required scopes enforced via both type and runtime
  • Prevents timing issues from invalid scope access

요약

스코프 기반 API 실행 제어를 강제하는 useQuery 래퍼 훅인 useScopedQuery를 도입합니다.

왜 필요한가요?

우리 서비스는 API Scope을 통해 리소스 접근 권한을 제어합니다. 하지만 이 Scope는 Route Scope와 약하게만 연결되어 있어,
올바른 Scope가 세팅되기 전에 Query가 먼저 실행되는 시점 문제가 발생할 수 있습니다.

이를 방지하기 위해 useScopedQuery는 다음을 보장합니다:

  • 유저의 권한이 유효할 때만 Query 실행
  • 글로벌 로딩 상태에서는 Query 자동 비활성화
  • enabled 값은 ref/computed도 허용되며 내부 로직과 병합
  • 개발 환경에서 Scope 차단 시 콘솔 경고 출력

주요 기능

  • 기존 useQuery 옵션과 완전 호환
  • 정적/반응형 enabled 모두 지원
  • requiredScopes는 타입 및 런타임에서 강제
  • 시점 이슈로 인한 권한 오류 예방

Things to Talk About (optional)

Copy link

vercel bot commented Apr 7, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

5 Skipped Deployments
Name Status Preview Comments Updated (UTC)
console ⬜️ Ignored (Inspect) Visit Preview Apr 14, 2025 1:53am
dashboard ⬜️ Ignored (Inspect) Visit Preview Apr 14, 2025 1:53am
feature-integration-project-detail ⬜️ Ignored (Inspect) Visit Preview Apr 14, 2025 1:53am
feature-project-landing ⬜️ Ignored (Inspect) Visit Preview Apr 14, 2025 1:53am
web-storybook ⬜️ Ignored (Inspect) Visit Preview Apr 14, 2025 1:53am

Copy link
Contributor

github-actions bot commented Apr 7, 2025

🎉 @WANZARGEN and @seungyeoneeee have been randomly selected as the reviewers! Please review. 🙏

Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot reviewed 13 out of 13 changed files in this pull request and generated no comments.

Comments suppressed due to low confidence (4)

apps/web/src/services/dashboards/composables/use-dashboard-query.ts:70

  • [nitpick] Removing explicit generic type parameters may affect type inference. Please verify that the new useScopedQuery implementation correctly infers types for queries.
const publicDashboardListQuery = useScopedQuery({

apps/web/src/services/dashboards/composables/use-dashboard-query.ts:79

  • [nitpick] Ensure that removing explicit generic parameters does not lead to issues with type inference for the private dashboard query.
const privateDashboardListQuery = useScopedQuery({

apps/web/src/query/composables/tests/use-scoped-query.test.ts:24

  • The test mock sets globalGrantLoading to true, causing isAppReady to be false and disabling the query. To test enabled queries properly, consider setting globalGrantLoading to false.
useAppContextStore: () => ({ getters: { globalGrantLoading: true } }),

apps/web/src/api-clients/_common/composables/use-scoped-query.ts:1

  • Ensure that all external references to the old useScopedQuery have been updated to the new implementation to prevent potential runtime errors.
// Removal of the old useScopedQuery file

Copy link
Member

@WANZARGEN WANZARGEN left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM~!

const {
queryKey, enabled, currentScope, requiredScopes, isAppReady,
} = params;
if (!enabled || !isAppReady || !currentScope) return;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: It doesn't warn in case that scope is invalid.


if (_warnedKeys.has(key)) return;

_warnedKeys.add(key);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Using Symbol for Warning Keys Management

Summary

Consider using Symbol to manage warning keys separately per context instead of using a global Set.

Current Implementation

const _warnedKeys = new Set<string>();

Suggested Implementation

const createWarnedKeysSymbol = () => Symbol('warnedKeys');

export const useScopedQuery = <T,...>(...) => {
    const WARNED_KEYS = createWarnedKeysSymbol();
    const warnedKeys = new Set<string>();
    (globalThis as any)[WARNED_KEYS] = warnedKeys;
    // ... rest of the implementation
}

Benefits

  • Isolates warning state per hook instance
  • Prevents warning key collisions across different components/contexts
  • More accurate warning tracking for identical query keys used in different contexts
  • Better memory management as warning sets can be garbage collected with their contexts

Considerations

  • Slightly increased memory usage due to multiple Sets
  • Need to ensure proper cleanup of global Symbol references
  • May want to consider WeakMap as an alternative for automatic garbage collection

Impact

Low risk, medium effort change that improves debugging accuracy and maintainability.

Testing Recommendations

  • Verify warnings work independently across different components
  • Check memory usage patterns
  • Ensure no warning leaks between different hook instances

What are your thoughts on this approach?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Korean]

경고 키(Warning Keys) 관리를 위한 Symbol 사용 제안

요약

현재 전역 Set으로 관리되는 경고 키를 Symbol을 사용하여 컨텍스트별로 분리하여 관리하는 방안을 제안드립니다.

현재 구현

const _warnedKeys = new Set<string>();

제안 구현

const createWarnedKeysSymbol = () => Symbol('warnedKeys');

export const useScopedQuery = <T,...>(...) => {
    const WARNED_KEYS = createWarnedKeysSymbol();
    const warnedKeys = new Set<string>();
    (globalThis as any)[WARNED_KEYS] = warnedKeys;
    // ... 나머지 구현
}

장점

  • 훅 인스턴스별로 경고 상태 격리
  • 서로 다른 컴포넌트/컨텍스트 간 경고 키 충돌 방지
  • 동일한 쿼리 키가 다른 컨텍스트에서 사용될 때 더 정확한 경고 추적 가능
  • 컨텍스트와 함께 가비지 컬렉션이 가능하여 메모리 관리가 개선됨

고려사항

  • 여러 Set 사용으로 인한 약간의 메모리 사용량 증가
  • 전역 Symbol 참조에 대한 적절한 정리 필요
  • 자동 가비지 컬렉션을 위해 WeakMap 사용도 대안으로 고려 가능

영향도

위험도는 낮고, 구현 노력은 중간 정도이며, 디버깅 정확성과 유지보수성이 향상됩니다.

테스트 권장사항

  • 서로 다른 컴포넌트에서 경고가 독립적으로 작동하는지 확인
  • 메모리 사용 패턴 확인
  • 서로 다른 훅 인스턴스 간 경고 누수가 없는지 확인

이 접근 방식에 대해 어떻게 생각하시나요?


if (_warnedKeys.has(key)) return;

_warnedKeys.add(key);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Korean]

경고 키(Warning Keys) 관리를 위한 Symbol 사용 제안

요약

현재 전역 Set으로 관리되는 경고 키를 Symbol을 사용하여 컨텍스트별로 분리하여 관리하는 방안을 제안드립니다.

현재 구현

const _warnedKeys = new Set<string>();

제안 구현

const createWarnedKeysSymbol = () => Symbol('warnedKeys');

export const useScopedQuery = <T,...>(...) => {
    const WARNED_KEYS = createWarnedKeysSymbol();
    const warnedKeys = new Set<string>();
    (globalThis as any)[WARNED_KEYS] = warnedKeys;
    // ... 나머지 구현
}

장점

  • 훅 인스턴스별로 경고 상태 격리
  • 서로 다른 컴포넌트/컨텍스트 간 경고 키 충돌 방지
  • 동일한 쿼리 키가 다른 컨텍스트에서 사용될 때 더 정확한 경고 추적 가능
  • 컨텍스트와 함께 가비지 컬렉션이 가능하여 메모리 관리가 개선됨

고려사항

  • 여러 Set 사용으로 인한 약간의 메모리 사용량 증가
  • 전역 Symbol 참조에 대한 적절한 정리 필요
  • 자동 가비지 컬렉션을 위해 WeakMap 사용도 대안으로 고려 가능

영향도

위험도는 낮고, 구현 노력은 중간 정도이며, 디버깅 정확성과 유지보수성이 향상됩니다.

테스트 권장사항

  • 서로 다른 컴포넌트에서 경고가 독립적으로 작동하는지 확인
  • 메모리 사용 패턴 확인
  • 서로 다른 훅 인스턴스 간 경고 누수가 없는지 확인

이 접근 방식에 대해 어떻게 생각하시나요?

@piggggggggy piggggggggy force-pushed the feature-scoped-query-composable branch from 74d8915 to 0520c37 Compare April 12, 2025 11:01
Copy link
Member Author

@piggggggggy piggggggggy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Revise dev warning logger for useScopedQuery, addressing both DX and conceptual clarity.

  1. Why not use getCurrentInstance()
  • getCurrentInstance() is designed for accessing Vue’s internal component context.
  • It does not reflect a query’s declaration location, making it conceptually unsuitable.
  • If the goal were to deduplicate logs per invocation, Symbol would be a more appropriate and consistent mechanism.
  1. Why Symbol wasn’t selected
  • Symbol-based logging fits a “per-call” model (each invocation is distinct),
  • But our goal was to log only once per declaration, not every time the query runs.
  1. Final approach: Error.stack + microtask tick
  • Uses Error().stack to infer the exact declaration point (file + line)
  • Uses queueMicrotask() to cache the warning for one tick
  • ✅ Prevents noisy duplicates
  • ✅ Allows re-logging on future re-entries
  • ✅ Aligns with a declarative model of usage

useScopedQuery 내 개발용 경고 로그 시스템을 다음과 같은 이유로 개선하였습니다:

  1. 인스턴스 기반 방식 (getCurrentInstance) — ❌ 사용하지 않음
  • 초기에는 getCurrentInstance()를 활용하여 컴포넌트 기준으로 로깅을 캐싱하려 했지만,
  • 이 API는 Vue의 내부 렌더링 컨텍스트나 라이프사이클 접근용이며,
  • 선언 위치 기반 로깅이라는 목적과는 거리가 있습니다.
  • 같은 목적이라면 Symbol을 통한 호출 단위 캐싱이 더 일관된 선택입니다.
  1. Symbol 기반 (시도) — ❌ 채택하지 않음
  • Symbol은 호출마다 고유한 값이 생성되므로, **“호출 시마다 로깅”**에 적합하지만,
  • 우리가 의도한 것은 **“선언 위치당 1회만 로깅”**하는 구조였기 때문에 부합하지 않았습니다.
  1. 최종 선택: Error.stack + tick 기반 캐싱 — ✅ 채택
  • Error().stack을 통해 쿼리 선언 위치를 추출해 고유 key 생성
  • queueMicrotask()를 이용해 이벤트 루프 단위로 로그 캐싱
  • → 동일 tick 내 중복 로그 방지
  • → 다음 tick에서 다시 로깅 가능 (페이지 재진입 대응)

@piggggggggy piggggggggy requested a review from Copilot April 12, 2025 18:04
Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot reviewed 13 out of 13 changed files in this pull request and generated no comments.

Comments suppressed due to low confidence (3)

apps/web/src/api-clients/_common/composables/use-scoped-query.ts:1

  • Ensure that no parts of the codebase rely on the old useScopedQuery import path; all references should be updated to use '@/query/composables/use-scoped-query'.
// Deprecated useScopedQuery implementation removed

apps/web/src/query/query-key/_types/query-key-type.ts:4

  • Verify that treating QueryKeyArray as immutable does not break any parts of the code that expect to modify the query key arrays.
export type QueryKeyArray = readonly unknown[];

apps/web/src/services/dashboards/composables/use-dashboard-query.ts:70

  • [nitpick] Consider providing explicit generic type arguments for useScopedQuery if TypeScript inference does not correctly capture the desired types for the query function.
const publicDashboardListQuery = useScopedQuery({

Signed-off-by: samuel.park <[email protected]>
@piggggggggy piggggggggy merged commit 26bdff8 into develop Apr 14, 2025
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants