원클릭으로
frontend-profile-batch-query
Use when fetching user profiles in bulk (workaround for PostgREST cross-schema join limitation)
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Use when fetching user profiles in bulk (workaround for PostgREST cross-schema join limitation)
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
Use when deploying Cloudflare Workers, managing R2 storage, or working with Cloudflare infrastructure
Use when working with ANTD components, theme tokens, icons, forms, or feedback components (message/notification/modal)
Use when adding, referencing, or serving static assets (images, fonts, videos, 3D models) through the R2 CDN pipeline with type-safe imports
Use when writing or reviewing JavaScript/TypeScript code for style patterns like concise arrows, inline handlers, expression formatting, or when tempted to use eslint-disable
Use when working with environment variables in frontend code
Use when creating or modifying keyboard shortcuts/hotkeys in frontend code
| name | frontend-profile-batch-query |
| description | Use when fetching user profiles in bulk (workaround for PostgREST cross-schema join limitation) |
Pattern for fetching multiple user profiles when PostgREST cannot join across schema boundaries.
Technical Constraint:
profiles.id → auth.users(id) (FK to auth schema)organization_members.user_id → auth.users(id) (FK to auth schema)profiles and organization_membersFailed approach:
// ❌ IMPOSSIBLE - PostgREST cannot traverse auth.users
supabase.from("organization_members").select(`
id,
user_id,
profiles ( // ❌ No direct FK relationship
email
)
`);
Error: Could not find a relationship between 'organization_members' and 'profiles' in the schema cache
Working approach:
// ✅ Batch query using .in() operator
supabase.from("profiles").select("id, email, whitelisted").in("id", userIds);
File: hooks/[Scope]/useQ_[Scope]_Profiles.ts
import { useQuery } from "@tanstack/react-query";
import { useMemo } from "react";
import { supabase, QueryData } from "@/configs/supabase/config";
import { QueryKeys } from "@/utils/query/queryKeys";
import useApp from "antd/es/app/useApp";
export type UseQ_[Scope]_Profiles_Params = {
userIds: string[];
enabled?: boolean;
};
const createProfilesQuery = (userIds: string[]) =>
supabase
.from("profiles")
.select(`
id,
email,
whitelisted,
whitelisted_at,
created_at,
updated_at
`)
.in("id", userIds);
export type [Scope]_Profiles_QueryData = QueryData<ReturnType<typeof createProfilesQuery>>;
export type [Scope]_Profile = [Scope]_Profiles_QueryData[number];
export const useQ_[Scope]_Profiles = (params: UseQ_[Scope]_Profiles_Params) => {
const { message } = useApp();
const { userIds, enabled = true } = params;
const query = useQuery({
enabled: enabled && userIds.length > 0,
queryKey: [...QueryKeys.profiles.all(), { userIds: [...userIds].sort() }] as const,
queryFn: async () => {
if (userIds.length === 0) return [];
const sb_FromProfiles_Select = await createProfilesQuery(userIds);
if (sb_FromProfiles_Select.error) {
console.error(sb_FromProfiles_Select.error);
message.error("Failed to fetch user profiles!");
throw sb_FromProfiles_Select.error;
}
return sb_FromProfiles_Select.data;
},
});
const profiles = useMemo(() => query.data || [], [query.data]);
const profilesMap = useMemo(
() =>
profiles.reduce(
(acc, profile) => {
acc[profile.id] = profile;
return acc;
},
{} as Record<string, [Scope]_Profile>
),
[profiles]
);
return {
query,
profiles,
profilesMap,
};
};
import { useMemo } from "react";
import { Table } from "antd";
import { useQ_[Scope]_ParentData } from "@/hooks/...";
import { useQ_[Scope]_Profiles } from "@/hooks/...";
function MyComponent() {
// Step 1: Fetch parent data with user_ids
const qParent = useQ_[Scope]_ParentData({ ... });
// Step 2: Extract user IDs
const userIds = useMemo(
() => qParent.items.map(item => item.user_id),
[qParent.items]
);
// Step 3: Batch fetch profiles
const qProfiles = useQ_[Scope]_Profiles({
userIds,
enabled: !qParent.query.isLoading
});
// Step 4: Loading states
if (qParent.query.isLoading || qProfiles.query.isLoading) {
return <Spin />;
}
if (qParent.query.error || qProfiles.query.error) {
return <Alert type="error" message="Failed to load data" />;
}
// Step 5: Use profilesMap for O(1) lookups
return (
<Table
dataSource={qParent.items}
columns={[
{
title: "Email",
render: (_, record) => {
const profile = qProfiles.profilesMap[record.user_id];
return <span>{profile?.email || "Loading..."}</span>;
},
},
]}
/>
);
}
const qProfiles = useQ_[Scope]_Profiles({
userIds,
enabled: !qParent.query.isLoading // Wait for parent to load
});
Why: Prevents firing query before userIds array is populated
queryKey: [...QueryKeys.profiles.all(), { userIds: [...userIds].sort() }];
Why: Same set of user IDs (different order) should hit same cache
if (userIds.length === 0) return [];
Why: Prevents invalid .in("id", []) query to database
const profilesMap = useMemo(
() =>
profiles.reduce(
(acc, profile) => ({ ...acc, [profile.id]: profile }),
{} as Record<string, Profile>
),
[profiles]
);
Why: O(1) lookup performance in renders instead of O(n) with .find()
Hook: src/hooks/PageOrganization/PageOrganization_SettingsUsers/useQ_PageOrganizationSettingsUsers_Profiles.ts
Component: src/pages/Page_Organization/PageOrganization_SettingsUsers.tsx
useQ_[Scope]_Profiles (plural){ userIds: string[], enabled?: boolean }.in("id", userIds) query{ query, profiles, profilesMap }